3531 implement bulk processors (#3566)
* base batch2 bulk export steps - unimplemented * added the basic steps * adding tests * we have added the tests * added tests for bulk export stages * implementing jpa * 3531 adding the jpabukexportprocessorimplementation * 3531 first step for mongo bulk export * jpa stuff updated * 3531 hapi fhir stuff * trying to fix up tests * fixing test * fixed most tests in batch export * deprecate * need some more guidance here * fixing some tests * some steps taken * blah * stashing * test * fixing tests round 1 * many a changes * updating version * small updates * merging in mster again * fixing up serialization * added the permission checkers * cleanup and sonarkube crap * some more tests * updating adding tests * crimes * blah * updatingmsg number again * removal of commented code * cleanup * merge confflicts * fixing msg * review fixes round 1 * more review fixes * merging in master * fixing poms * review points * test fix * test fix * fix test race condition * truncate error * add stack trace to error * rename to IT * fix race condition in test * finally fixed race condition in test. this time I'm optimistic hapi ci will finally pass * fix mock test. not a fan of mock tests like this. * any job definition with a reducer step must be gated. * fixed common batch bean wiring * fix test * lazy init ResponseTerminologyTranslationSvc * change the way we lazy init ResponseTerminologyTranslationSvc * change the way we lazy init ResponseTerminologyTranslationSvc (for the third time. this is super tricky!) * disabling testGoldenResourceWithCircularReferenceCanBeCleared for now Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-MacBook-Pro.local> Co-authored-by: Ken Stevens <ken@smilecdr.com>
This commit is contained in:
parent
398ed99f86
commit
8ed0a18ef4
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ public final class Msg {
|
|||
|
||||
/**
|
||||
* IMPORTANT: Please update the following comment after you add a new code
|
||||
* Last code value: 2101
|
||||
* Last code value: 2106
|
||||
*/
|
||||
|
||||
private Msg() {}
|
||||
|
|
|
@ -28,10 +28,12 @@ import java.util.HashMap;
|
|||
*/
|
||||
public enum PreferReturnEnum {
|
||||
|
||||
REPRESENTATION(Constants.HEADER_PREFER_RETURN_REPRESENTATION), MINIMAL(Constants.HEADER_PREFER_RETURN_MINIMAL), OPERATION_OUTCOME(Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
REPRESENTATION(Constants.HEADER_PREFER_RETURN_REPRESENTATION),
|
||||
MINIMAL(Constants.HEADER_PREFER_RETURN_MINIMAL),
|
||||
OPERATION_OUTCOME(Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
private static HashMap<String, PreferReturnEnum> ourValues;
|
||||
private String myHeaderValue;
|
||||
private final String myHeaderValue;
|
||||
|
||||
PreferReturnEnum(String theHeaderValue) {
|
||||
myHeaderValue = theHeaderValue;
|
||||
|
|
|
@ -71,9 +71,6 @@ ca.uhn.fhir.validation.FhirValidator.noPhError=Ph-schematron library not found o
|
|||
ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected during validation
|
||||
|
||||
# JPA Messages
|
||||
|
||||
ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl.onlyBinarySelected=Binary resources may not be exported with bulk export
|
||||
ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl.unknownResourceType=Unknown or unsupported resource type: {0}
|
||||
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request.
|
||||
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index.
|
||||
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.forcedIdConstraintFailure=The operation has failed with a client-assigned ID constraint failure. This typically means that multiple client threads are trying to create a new resource with the same client-assigned ID at the same time, and this thread was chosen to be rejected. It can also happen when a request disables the Upsert Existence Check.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -49,7 +49,6 @@ public class NonPersistedBatchConfigurer extends DefaultBatchConfigurer {
|
|||
return myHapiPlatformTransactionManager;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected JobRepository createJobRepository() throws Exception {
|
||||
MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean();
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-bom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>HAPI FHIR BOM</name>
|
||||
|
||||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
@ -36,46 +36,8 @@
|
|||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<!-- For releases, we need to generate javadoc and sources JAR -->
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<activation>
|
||||
<activeByDefault>false</activeByDefault>
|
||||
<property>
|
||||
<name>deployToSonatype</name>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.batch2.jobs.imprt.BulkImportJobParameters;
|
|||
import ca.uhn.fhir.batch2.jobs.imprt.BulkDataImportProvider;
|
||||
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
|
||||
import ca.uhn.fhir.test.utilities.HttpClientExtension;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
|
@ -81,6 +82,12 @@ public class BulkImportCommandTest {
|
|||
BulkImportCommand.setEndNowForUnitTest(true);
|
||||
}
|
||||
|
||||
private Batch2JobStartResponse createJobStartResponse(String theId) {
|
||||
Batch2JobStartResponse response = new Batch2JobStartResponse();
|
||||
response.setJobId(theId);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkImport() throws IOException {
|
||||
|
||||
|
@ -89,7 +96,8 @@ public class BulkImportCommandTest {
|
|||
writeNdJsonFileToTempDirectory(fileContents1, "file1.json");
|
||||
writeNdJsonFileToTempDirectory(fileContents2, "file2.json");
|
||||
|
||||
when(myJobCoordinator.startInstance(any())).thenReturn("THE-JOB-ID");
|
||||
when(myJobCoordinator.startInstance(any()))
|
||||
.thenReturn(createJobStartResponse("THE-JOB-ID"));
|
||||
|
||||
// Start the command in a separate thread
|
||||
new Thread(() -> App.main(new String[]{
|
||||
|
@ -123,7 +131,8 @@ public class BulkImportCommandTest {
|
|||
writeNdJsonFileToTempDirectory(fileContents1, "file1.json.gz");
|
||||
writeNdJsonFileToTempDirectory(fileContents2, "file2.json.gz");
|
||||
|
||||
when(myJobCoordinator.startInstance(any())).thenReturn("THE-JOB-ID");
|
||||
when(myJobCoordinator.startInstance(any()))
|
||||
.thenReturn(createJobStartResponse("THE-JOB-ID"));
|
||||
|
||||
// Start the command in a separate thread
|
||||
new Thread(() -> App.main(new String[]{
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-cli</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ package ca.uhn.fhir.jpa.batch;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.batch.processor.GoldenResourceAnnotatingProcessor;
|
||||
import ca.uhn.fhir.jpa.batch.processor.PidToIBaseResourceProcessor;
|
||||
import org.springframework.batch.core.configuration.annotation.StepScope;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -30,12 +29,6 @@ import org.springframework.context.annotation.Configuration;
|
|||
public class CommonBatchJobConfig {
|
||||
public static final int MINUTES_IN_FUTURE_TO_PROCESS_FROM = 1;
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public PidToIBaseResourceProcessor pidToResourceProcessor() {
|
||||
return new PidToIBaseResourceProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public GoldenResourceAnnotatingProcessor goldenResourceAnnotatingProcessor() {
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.batch.processor;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.batch.log.Logs;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.batch.item.ItemProcessor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Reusable Item Processor which converts ResourcePersistentIds to their IBaseResources
|
||||
*/
|
||||
public class PidToIBaseResourceProcessor implements ItemProcessor<List<ResourcePersistentId>, List<IBaseResource>> {
|
||||
private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
|
||||
|
||||
@Autowired
|
||||
private SearchBuilderFactory mySearchBuilderFactory;
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
|
||||
@Value("#{stepExecutionContext['resourceType']}")
|
||||
private String myResourceType;
|
||||
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
|
||||
@Override
|
||||
public List<IBaseResource> process(List<ResourcePersistentId> theResourcePersistentId) {
|
||||
String collect = theResourcePersistentId.stream().map(pid -> pid.getId().toString()).collect(Collectors.joining(","));
|
||||
ourLog.trace("Processing PIDs: {}" + collect);
|
||||
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(myResourceType);
|
||||
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(myResourceType).getImplementingClass();
|
||||
|
||||
ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(dao, myResourceType, resourceTypeClass);
|
||||
List<IBaseResource> outgoing = new ArrayList<>();
|
||||
sb.loadResourcesByPid(theResourcePersistentId, Collections.emptyList(), outgoing, false, null);
|
||||
|
||||
ourLog.trace("Loaded resources: {}", outgoing.stream().filter(t -> t != null).map(t -> t.getIdElement().getValue()).collect(Collectors.joining(", ")));
|
||||
|
||||
return outgoing;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.batch2;
|
|||
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
||||
import ca.uhn.fhir.batch2.api.JobOperationResultJson;
|
||||
import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk;
|
||||
import ca.uhn.fhir.batch2.model.FetchJobInstancesRequest;
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.MarkWorkChunkAsErrorRequest;
|
||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
|
@ -31,10 +32,14 @@ import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
|
|||
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
|
||||
import ca.uhn.fhir.jpa.util.JobInstanceUtil;
|
||||
import ca.uhn.fhir.model.api.PagingIterator;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -53,6 +58,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
|
|||
|
||||
@Transactional
|
||||
public class JpaJobPersistenceImpl implements IJobPersistence {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(JpaJobPersistenceImpl.class);
|
||||
|
||||
private final IBatch2JobInstanceRepository myJobInstanceRepository;
|
||||
private final IBatch2WorkChunkRepository myWorkChunkRepository;
|
||||
|
@ -78,6 +84,7 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
entity.setInstanceId(theBatchWorkChunk.instanceId);
|
||||
entity.setSerializedData(theBatchWorkChunk.serializedData);
|
||||
entity.setCreateTime(new Date());
|
||||
entity.setStartTime(new Date());
|
||||
entity.setStatus(StatusEnum.QUEUED);
|
||||
myWorkChunkRepository.save(entity);
|
||||
return entity.getId();
|
||||
|
@ -102,6 +109,7 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
entity.setParams(theInstance.getParameters());
|
||||
entity.setCurrentGatedStepId(theInstance.getCurrentGatedStepId());
|
||||
entity.setCreateTime(new Date());
|
||||
entity.setStartTime(new Date());
|
||||
entity.setReport(theInstance.getReport());
|
||||
|
||||
entity = myJobInstanceRepository.save(entity);
|
||||
|
@ -135,6 +143,23 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
return myJobInstanceRepository.findById(theInstanceId).map(t -> toInstance(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JobInstance> fetchInstances(FetchJobInstancesRequest theRequest, int theStart, int theBatchSize) {
|
||||
String definitionId = theRequest.getJobDefinition();
|
||||
String params = theRequest.getParameters();
|
||||
|
||||
Pageable pageable = Pageable.ofSize(theBatchSize).withPage(theStart);
|
||||
|
||||
// TODO - consider adding a new index... on the JobDefinitionId (and possibly Status)
|
||||
List<JobInstance> instances = myJobInstanceRepository.findInstancesByJobIdAndParams(
|
||||
definitionId,
|
||||
params,
|
||||
pageable
|
||||
);
|
||||
|
||||
return instances == null ? new ArrayList<>() : instances;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JobInstance> fetchInstances(int thePageSize, int thePageIndex) {
|
||||
// default sort is myCreateTime Asc
|
||||
|
@ -149,49 +174,11 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
}
|
||||
|
||||
private WorkChunk toChunk(Batch2WorkChunkEntity theEntity, boolean theIncludeData) {
|
||||
WorkChunk retVal = new WorkChunk();
|
||||
retVal.setId(theEntity.getId());
|
||||
retVal.setSequence(theEntity.getSequence());
|
||||
retVal.setJobDefinitionId(theEntity.getJobDefinitionId());
|
||||
retVal.setJobDefinitionVersion(theEntity.getJobDefinitionVersion());
|
||||
retVal.setInstanceId(theEntity.getInstanceId());
|
||||
retVal.setTargetStepId(theEntity.getTargetStepId());
|
||||
retVal.setStatus(theEntity.getStatus());
|
||||
retVal.setCreateTime(theEntity.getCreateTime());
|
||||
retVal.setStartTime(theEntity.getStartTime());
|
||||
retVal.setEndTime(theEntity.getEndTime());
|
||||
retVal.setErrorMessage(theEntity.getErrorMessage());
|
||||
retVal.setErrorCount(theEntity.getErrorCount());
|
||||
retVal.setRecordsProcessed(theEntity.getRecordsProcessed());
|
||||
if (theIncludeData) {
|
||||
if (theEntity.getSerializedData() != null) {
|
||||
retVal.setData(theEntity.getSerializedData());
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
return JobInstanceUtil.fromEntityToWorkChunk(theEntity, theIncludeData);
|
||||
}
|
||||
|
||||
private JobInstance toInstance(Batch2JobInstanceEntity theEntity) {
|
||||
JobInstance retVal = JobInstance.fromInstanceId(theEntity.getId());
|
||||
retVal.setJobDefinitionId(theEntity.getDefinitionId());
|
||||
retVal.setJobDefinitionVersion(theEntity.getDefinitionVersion());
|
||||
retVal.setStatus(theEntity.getStatus());
|
||||
retVal.setCancelled(theEntity.isCancelled());
|
||||
retVal.setStartTime(theEntity.getStartTime());
|
||||
retVal.setCreateTime(theEntity.getCreateTime());
|
||||
retVal.setEndTime(theEntity.getEndTime());
|
||||
retVal.setCombinedRecordsProcessed(theEntity.getCombinedRecordsProcessed());
|
||||
retVal.setCombinedRecordsProcessedPerSecond(theEntity.getCombinedRecordsProcessedPerSecond());
|
||||
retVal.setTotalElapsedMillis(theEntity.getTotalElapsedMillis());
|
||||
retVal.setWorkChunksPurged(theEntity.getWorkChunksPurged());
|
||||
retVal.setProgress(theEntity.getProgress());
|
||||
retVal.setErrorMessage(theEntity.getErrorMessage());
|
||||
retVal.setErrorCount(theEntity.getErrorCount());
|
||||
retVal.setEstimatedTimeRemaining(theEntity.getEstimatedTimeRemaining());
|
||||
retVal.setParameters(theEntity.getParams());
|
||||
retVal.setCurrentGatedStepId(theEntity.getCurrentGatedStepId());
|
||||
retVal.setReport(theEntity.getReport());
|
||||
return retVal;
|
||||
return JobInstanceUtil.fromEntityToInstance(theEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -209,7 +196,14 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
|
||||
@Override
|
||||
public void markWorkChunkAsFailed(String theChunkId, String theErrorMessage) {
|
||||
myWorkChunkRepository.updateChunkStatusAndIncrementErrorCountForEndError(theChunkId, new Date(), theErrorMessage, StatusEnum.FAILED);
|
||||
String errorMessage;
|
||||
if (theErrorMessage.length() > Batch2WorkChunkEntity.ERROR_MSG_MAX_LENGTH) {
|
||||
ourLog.warn("Truncating error message that is too long to store in database: {}", theErrorMessage);
|
||||
errorMessage = theErrorMessage.substring(0, Batch2WorkChunkEntity.ERROR_MSG_MAX_LENGTH);
|
||||
} else {
|
||||
errorMessage = theErrorMessage;
|
||||
}
|
||||
myWorkChunkRepository.updateChunkStatusAndIncrementErrorCountForEndError(theChunkId, new Date(), errorMessage, StatusEnum.FAILED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -244,11 +238,24 @@ public class JpaJobPersistenceImpl implements IJobPersistence {
|
|||
}
|
||||
}
|
||||
|
||||
private void fetchChunksForStep(String theInstanceId, String theStepId, int thePageSize, int thePageIndex, Consumer<WorkChunk> theConsumer) {
|
||||
List<Batch2WorkChunkEntity> chunks = myWorkChunkRepository.fetchChunksForStep(PageRequest.of(thePageIndex, thePageSize), theInstanceId, theStepId);
|
||||
for (Batch2WorkChunkEntity chunk : chunks) {
|
||||
theConsumer.accept(toChunk(chunk, true));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<WorkChunk> fetchAllWorkChunksIterator(String theInstanceId, boolean theWithData) {
|
||||
return new PagingIterator<>((thePageIndex, theBatchSize, theConsumer) -> fetchChunks(theInstanceId, theWithData, theBatchSize, thePageIndex, theConsumer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<WorkChunk> fetchAllWorkChunksForStepIterator(String theInstanceId, String theStepId) {
|
||||
return new PagingIterator<>((thePageIndex, theBatchSize, theConsumer) -> fetchChunksForStep(theInstanceId, theStepId, theBatchSize, thePageIndex, theConsumer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stored instance
|
||||
*
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class BaseJpaBulkItemReader extends BaseBulkItemReader {
|
||||
@Value("#{jobExecutionContext['" + BatchConstants.JOB_UUID_PARAMETER + "']}")
|
||||
protected String myJobUUID;
|
||||
@Autowired
|
||||
protected DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
protected SearchBuilderFactory mySearchBuilderFactory;
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
|
||||
private ISearchBuilder mySearchBuilder;
|
||||
private BulkExportJobEntity myJobEntity;
|
||||
|
||||
private RuntimeSearchParam myPatientSearchParam;
|
||||
|
||||
/**
|
||||
* Get and cache an ISearchBuilder for the given resource type this partition is responsible for.
|
||||
*/
|
||||
protected ISearchBuilder getSearchBuilderForLocalResourceType() {
|
||||
if (mySearchBuilder == null) {
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(myResourceType);
|
||||
RuntimeResourceDefinition def = myContext.getResourceDefinition(myResourceType);
|
||||
Class<? extends IBaseResource> nextTypeClass = def.getImplementingClass();
|
||||
mySearchBuilder = mySearchBuilderFactory.newSearchBuilder(dao, myResourceType, nextTypeClass);
|
||||
}
|
||||
return mySearchBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getTypeFilterList() {
|
||||
BulkExportJobEntity jobEntity = getJobEntity();
|
||||
Map<String, String[]> requestUrl = UrlUtil.parseQueryStrings(jobEntity.getRequest());
|
||||
return requestUrl.get(JpaConstants.PARAM_EXPORT_TYPE_FILTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Date getSinceDate() {
|
||||
return getJobEntity().getSince();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLogInfoForRead() {
|
||||
return "Bulk export starting generation for batch export job: " + getJobEntity() + " with resourceType " + myResourceType + " and UUID " + myJobUUID;
|
||||
}
|
||||
|
||||
protected BulkExportJobEntity getJobEntity() {
|
||||
if (myJobEntity == null) {
|
||||
Optional<BulkExportJobEntity> jobOpt = myBulkExportJobDao.findByJobId(myJobUUID);
|
||||
if (jobOpt.isPresent()) {
|
||||
myJobEntity = jobOpt.get();
|
||||
} else {
|
||||
String errorMessage = String.format("Job with UUID %s does not exist!", myJobUUID);
|
||||
throw new IllegalStateException(Msg.code(795) + errorMessage);
|
||||
}
|
||||
}
|
||||
return myJobEntity;
|
||||
}
|
||||
|
||||
protected RuntimeSearchParam getPatientSearchParamForCurrentResourceType() {
|
||||
if (myPatientSearchParam == null) {
|
||||
Optional<RuntimeSearchParam> onlyPatientSearchParamForResourceType = SearchParameterUtil.getOnlyPatientSearchParamForResourceType(myContext, myResourceType);
|
||||
if (onlyPatientSearchParamForResourceType.isPresent()) {
|
||||
myPatientSearchParam = onlyPatientSearchParamForResourceType.get();
|
||||
}
|
||||
}
|
||||
return myPatientSearchParam;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import org.springframework.batch.core.ExitStatus;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
import org.springframework.batch.core.StepExecutionListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Will run before and after a job to set the status to whatever is appropriate.
|
||||
*/
|
||||
public class BulkExportCreateEntityStepListener implements StepExecutionListener {
|
||||
|
||||
@Autowired
|
||||
private BulkExportDaoSvc myBulkExportDaoSvc;
|
||||
|
||||
@Override
|
||||
public void beforeStep(StepExecution theStepExecution) {
|
||||
String jobUuid = theStepExecution.getJobExecution().getJobParameters().getString("jobUUID");
|
||||
if (jobUuid != null) {
|
||||
myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkExportJobStatusEnum.BUILDING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus afterStep(StepExecution theStepExecution) {
|
||||
return ExitStatus.EXECUTING;
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import org.springframework.batch.core.ExitStatus;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
import org.springframework.batch.core.StepExecutionListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* This class sets the job status to ERROR if any failures occur while actually
|
||||
* generating the export files.
|
||||
*/
|
||||
public class BulkExportGenerateResourceFilesStepListener implements StepExecutionListener {
|
||||
|
||||
@Autowired
|
||||
private BulkExportDaoSvc myBulkExportDaoSvc;
|
||||
|
||||
@Override
|
||||
public void beforeStep(@Nonnull StepExecution stepExecution) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus afterStep(StepExecution theStepExecution) {
|
||||
if (theStepExecution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode())) {
|
||||
//Try to fetch it from the parameters first, and if it doesn't exist, fetch it from the context.
|
||||
String jobUuid = theStepExecution.getJobExecution().getJobParameters().getString(BatchConstants.JOB_UUID_PARAMETER);
|
||||
if (jobUuid == null) {
|
||||
jobUuid = theStepExecution.getJobExecution().getExecutionContext().getString(BatchConstants.JOB_UUID_PARAMETER);
|
||||
}
|
||||
assert isNotBlank(jobUuid);
|
||||
String exitDescription = theStepExecution.getExitStatus().getExitDescription();
|
||||
myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkExportJobStatusEnum.ERROR, exitDescription);
|
||||
}
|
||||
return theStepExecution.getExitStatus();
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import org.springframework.batch.core.BatchStatus;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
/**
|
||||
* Will run before and after a job to set the status to whatever is appropriate.
|
||||
*/
|
||||
public class BulkExportJobCloser implements Tasklet {
|
||||
|
||||
@Value("#{jobExecutionContext['jobUUID']}")
|
||||
private String myJobUUID;
|
||||
|
||||
@Autowired
|
||||
private BulkExportDaoSvc myBulkExportDaoSvc;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution theStepContribution, ChunkContext theChunkContext) {
|
||||
if (theChunkContext.getStepContext().getStepExecution().getJobExecution().getStatus() == BatchStatus.STARTED) {
|
||||
myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkExportJobStatusEnum.COMPLETE);
|
||||
} else {
|
||||
myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkExportJobStatusEnum.ERROR);
|
||||
}
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
|
@ -20,31 +20,9 @@ package ca.uhn.fhir.jpa.bulk.export.job;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.batch.processor.GoldenResourceAnnotatingProcessor;
|
||||
import ca.uhn.fhir.jpa.batch.processor.PidToIBaseResourceProcessor;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobParametersValidator;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
|
||||
import org.springframework.batch.core.configuration.annotation.JobScope;
|
||||
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
|
||||
import org.springframework.batch.core.configuration.annotation.StepScope;
|
||||
import org.springframework.batch.item.ItemProcessor;
|
||||
import org.springframework.batch.item.support.CompositeItemProcessor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring batch Job configuration file. Contains all necessary plumbing to run a
|
||||
|
@ -54,220 +32,10 @@ import java.util.List;
|
|||
public class BulkExportJobConfig {
|
||||
public static final int CHUNK_SIZE = 100;
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Autowired
|
||||
private StepBuilderFactory myStepBuilderFactory;
|
||||
|
||||
@Autowired
|
||||
private JobBuilderFactory myJobBuilderFactory;
|
||||
|
||||
@Autowired
|
||||
private PidToIBaseResourceProcessor myPidToIBaseResourceProcessor;
|
||||
|
||||
@Autowired
|
||||
private GoldenResourceAnnotatingProcessor myGoldenResourceAnnotatingProcessor;
|
||||
|
||||
@Bean
|
||||
public BulkExportDaoSvc bulkExportDaoSvc() {
|
||||
return new BulkExportDaoSvc();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@JobScope
|
||||
public MdmExpansionCacheSvc mdmExpansionCacheSvc() {
|
||||
return new MdmExpansionCacheSvc();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public Job bulkExportJob() {
|
||||
return myJobBuilderFactory.get(BatchConstants.BULK_EXPORT_JOB_NAME)
|
||||
.validator(bulkExportJobParameterValidator())
|
||||
.start(createBulkExportEntityStep())
|
||||
.next(bulkExportPartitionStep())
|
||||
.next(closeJobStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@StepScope
|
||||
public CompositeItemProcessor<List<ResourcePersistentId>, List<IBaseResource>> inflateResourceThenAnnotateWithGoldenResourceProcessor() {
|
||||
CompositeItemProcessor processor = new CompositeItemProcessor<>();
|
||||
ArrayList<ItemProcessor> delegates = new ArrayList<>();
|
||||
delegates.add(myPidToIBaseResourceProcessor);
|
||||
delegates.add(myGoldenResourceAnnotatingProcessor);
|
||||
processor.setDelegates(delegates);
|
||||
return processor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public Job groupBulkExportJob() {
|
||||
return myJobBuilderFactory.get(BatchConstants.GROUP_BULK_EXPORT_JOB_NAME)
|
||||
.validator(groupBulkJobParameterValidator())
|
||||
.validator(bulkExportJobParameterValidator())
|
||||
.start(createBulkExportEntityStep())
|
||||
.next(groupPartitionStep())
|
||||
.next(closeJobStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public Job patientBulkExportJob() {
|
||||
return myJobBuilderFactory.get(BatchConstants.PATIENT_BULK_EXPORT_JOB_NAME)
|
||||
.validator(bulkExportJobParameterValidator())
|
||||
.start(createBulkExportEntityStep())
|
||||
.next(patientPartitionStep())
|
||||
.next(closeJobStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupIdPresentValidator groupBulkJobParameterValidator() {
|
||||
return new GroupIdPresentValidator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step createBulkExportEntityStep() {
|
||||
return myStepBuilderFactory.get("createBulkExportEntityStep")
|
||||
.tasklet(createBulkExportEntityTasklet())
|
||||
.listener(bulkExportCreateEntityStepListener())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CreateBulkExportEntityTasklet createBulkExportEntityTasklet() {
|
||||
return new CreateBulkExportEntityTasklet();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public JobParametersValidator bulkExportJobParameterValidator() {
|
||||
return new BulkExportJobParameterValidator();
|
||||
}
|
||||
|
||||
//Writers
|
||||
@Bean
|
||||
public Step groupBulkExportGenerateResourceFilesStep() {
|
||||
return myStepBuilderFactory.get("groupBulkExportGenerateResourceFilesStep")
|
||||
.<List<ResourcePersistentId>, List<IBaseResource>>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time.
|
||||
.reader(groupBulkItemReader())
|
||||
.processor(inflateResourceThenAnnotateWithGoldenResourceProcessor())
|
||||
.writer(resourceToFileWriter())
|
||||
.listener(bulkExportGenerateResourceFilesStepListener())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step bulkExportGenerateResourceFilesStep() {
|
||||
return myStepBuilderFactory.get(BatchConstants.BULK_EXPORT_GENERATE_RESOURCE_FILES_STEP)
|
||||
.<List<ResourcePersistentId>, List<IBaseResource>>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time.
|
||||
.reader(bulkItemReader())
|
||||
.processor(myPidToIBaseResourceProcessor)
|
||||
.writer(resourceToFileWriter())
|
||||
.listener(bulkExportGenerateResourceFilesStepListener())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step patientBulkExportGenerateResourceFilesStep() {
|
||||
return myStepBuilderFactory.get("patientBulkExportGenerateResourceFilesStep")
|
||||
.<List<ResourcePersistentId>, List<IBaseResource>>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time.
|
||||
.reader(patientBulkItemReader())
|
||||
.processor(myPidToIBaseResourceProcessor)
|
||||
.writer(resourceToFileWriter())
|
||||
.listener(bulkExportGenerateResourceFilesStepListener())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@JobScope
|
||||
public BulkExportJobCloser bulkExportJobCloser() {
|
||||
return new BulkExportJobCloser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step closeJobStep() {
|
||||
return myStepBuilderFactory.get("closeJobStep")
|
||||
.tasklet(bulkExportJobCloser())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@JobScope
|
||||
public BulkExportCreateEntityStepListener bulkExportCreateEntityStepListener() {
|
||||
return new BulkExportCreateEntityStepListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@JobScope
|
||||
public BulkExportGenerateResourceFilesStepListener bulkExportGenerateResourceFilesStepListener() {
|
||||
return new BulkExportGenerateResourceFilesStepListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step bulkExportPartitionStep() {
|
||||
return myStepBuilderFactory.get("partitionStep")
|
||||
.partitioner(BatchConstants.BULK_EXPORT_GENERATE_RESOURCE_FILES_STEP, bulkExportResourceTypePartitioner())
|
||||
.step(bulkExportGenerateResourceFilesStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step groupPartitionStep() {
|
||||
return myStepBuilderFactory.get("partitionStep")
|
||||
.partitioner("groupBulkExportGenerateResourceFilesStep", bulkExportResourceTypePartitioner())
|
||||
.step(groupBulkExportGenerateResourceFilesStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Step patientPartitionStep() {
|
||||
return myStepBuilderFactory.get("partitionStep")
|
||||
.partitioner("patientBulkExportGenerateResourceFilesStep", bulkExportResourceTypePartitioner())
|
||||
.step(patientBulkExportGenerateResourceFilesStep())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public GroupBulkItemReader groupBulkItemReader() {
|
||||
return new GroupBulkItemReader();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public PatientBulkItemReader patientBulkItemReader() {
|
||||
return new PatientBulkItemReader();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public BulkItemReader bulkItemReader() {
|
||||
return new BulkItemReader();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@JobScope
|
||||
public ResourceTypePartitioner bulkExportResourceTypePartitioner() {
|
||||
return new ResourceTypePartitioner();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@StepScope
|
||||
public ResourceToFileWriter resourceToFileWriter() {
|
||||
return new ResourceToFileWriter(myFhirContext, myDaoRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.JobParametersInvalidException;
|
||||
import org.springframework.batch.core.JobParametersValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class will prevent a job from running if the UUID does not exist or is invalid.
|
||||
*/
|
||||
public class BulkExportJobParameterValidator implements JobParametersValidator {
|
||||
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
@Autowired
|
||||
private PlatformTransactionManager myTransactionManager;
|
||||
|
||||
@Override
|
||||
public void validate(JobParameters theJobParameters) throws JobParametersInvalidException {
|
||||
if (theJobParameters == null) {
|
||||
throw new JobParametersInvalidException(Msg.code(793) + "This job needs Parameters: [readChunkSize], [jobUUID], [filters], [outputFormat], [resourceTypes]");
|
||||
}
|
||||
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
|
||||
String errorMessage = txTemplate.execute(tx -> {
|
||||
StringBuilder errorBuilder = new StringBuilder();
|
||||
Long readChunkSize = theJobParameters.getLong(BatchConstants.READ_CHUNK_PARAMETER);
|
||||
if (readChunkSize == null || readChunkSize < 1) {
|
||||
errorBuilder.append("There must be a valid number for readChunkSize, which is at least 1. ");
|
||||
}
|
||||
String jobUUID = theJobParameters.getString(BatchConstants.JOB_UUID_PARAMETER);
|
||||
Optional<BulkExportJobEntity> oJob = myBulkExportJobDao.findByJobId(jobUUID);
|
||||
if (!StringUtils.isBlank(jobUUID) && !oJob.isPresent()) {
|
||||
errorBuilder.append("There is no persisted job that exists with UUID: " + jobUUID + ". ");
|
||||
}
|
||||
|
||||
|
||||
boolean hasExistingJob = oJob.isPresent();
|
||||
//Check for to-be-created parameters.
|
||||
if (!hasExistingJob) {
|
||||
String resourceTypes = theJobParameters.getString(BatchConstants.JOB_RESOURCE_TYPES_PARAMETER);
|
||||
if (StringUtils.isBlank(resourceTypes)) {
|
||||
errorBuilder.append("You must include [").append(BatchConstants.JOB_RESOURCE_TYPES_PARAMETER).append("] as a Job Parameter");
|
||||
} else {
|
||||
String[] resourceArray = resourceTypes.split(",");
|
||||
Arrays.stream(resourceArray).filter(resourceType -> resourceType.equalsIgnoreCase("Binary"))
|
||||
.findFirst()
|
||||
.ifPresent(resourceType -> errorBuilder.append("Bulk export of Binary resources is forbidden"));
|
||||
}
|
||||
|
||||
String outputFormat = theJobParameters.getString("outputFormat");
|
||||
if (!StringUtils.isBlank(outputFormat) && !Constants.CT_FHIR_NDJSON.equals(outputFormat)) {
|
||||
errorBuilder.append("The only allowed format for Bulk Export is currently " + Constants.CT_FHIR_NDJSON);
|
||||
}
|
||||
}
|
||||
|
||||
return errorBuilder.toString();
|
||||
});
|
||||
|
||||
if (!StringUtils.isEmpty(errorMessage)) {
|
||||
throw new JobParametersInvalidException(Msg.code(794) + errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.batch.log.Logs;
|
||||
import ca.uhn.fhir.jpa.dao.IResultIterator;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Basic Bulk Export implementation which simply reads all type filters and applies them, along with the _since param
|
||||
* on a given resource type.
|
||||
*/
|
||||
public class BulkItemReader extends BaseJpaBulkItemReader {
|
||||
private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
|
||||
|
||||
@Override
|
||||
protected Iterator<ResourcePersistentId> getResourcePidIterator() {
|
||||
ourLog.info("Bulk export assembling export of type {} for job {}", myResourceType, myJobUUID);
|
||||
Set<ResourcePersistentId> myReadPids = new HashSet<>();
|
||||
|
||||
List<SearchParameterMap> map = createSearchParameterMapsForResourceType();
|
||||
ISearchBuilder sb = getSearchBuilderForLocalResourceType();
|
||||
|
||||
for (SearchParameterMap spMap: map) {
|
||||
ourLog.debug("About to evaluate query {}", spMap.toNormalizedQueryString(myContext));
|
||||
IResultIterator myResultIterator = sb.createQuery(spMap, new SearchRuntimeDetails(null, myJobUUID), null, RequestPartitionId.allPartitions());
|
||||
while (myResultIterator.hasNext()) {
|
||||
myReadPids.add(myResultIterator.next());
|
||||
}
|
||||
}
|
||||
return myReadPids.iterator();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,332 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.batch.log.Logs;
|
||||
import ca.uhn.fhir.jpa.dao.IResultIterator;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IJpaIdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.util.QueryChunker;
|
||||
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.param.HasOrListParam;
|
||||
import ca.uhn.fhir.rest.param.HasParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.batch.item.ItemReader;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.jpa.batch.config.BatchConstants.PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES;
|
||||
|
||||
/**
|
||||
* Bulk Item reader for the Group Bulk Export job.
|
||||
* Instead of performing a normal query on the resource type using type filters, we instead
|
||||
*
|
||||
* 1. Get the group ID defined for this job
|
||||
* 2. Expand its membership so we get references to all patients in the group
|
||||
* 3. Optionally further expand that into all MDM-matched Patients (including golden resources)
|
||||
* 4. Then perform normal bulk export, filtered so that only results that refer to members are returned.
|
||||
*/
|
||||
public class GroupBulkItemReader extends BaseJpaBulkItemReader implements ItemReader<List<ResourcePersistentId>> {
|
||||
private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
|
||||
public static final int QUERY_CHUNK_SIZE = 100;
|
||||
|
||||
@Value("#{jobParameters['" + BatchConstants.GROUP_ID_PARAMETER + "']}")
|
||||
private String myGroupId;
|
||||
@Value("#{jobParameters['" + BatchConstants.EXPAND_MDM_PARAMETER+ "'] ?: false}")
|
||||
private boolean myMdmEnabled;
|
||||
|
||||
@Autowired
|
||||
private IIdHelperService myIdHelperService;
|
||||
@Autowired
|
||||
private IMdmLinkDao myMdmLinkDao;
|
||||
@Autowired
|
||||
private MdmExpansionCacheSvc myMdmExpansionCacheSvc;
|
||||
@Autowired
|
||||
private IJpaIdHelperService myJpaIdHelperService;
|
||||
|
||||
@Override
|
||||
protected Iterator<ResourcePersistentId> getResourcePidIterator() {
|
||||
//Short circuit out if we detect we are attempting to export groups.
|
||||
if (myResourceType.equalsIgnoreCase("Group")) {
|
||||
return getSingletonGroupIterator();
|
||||
}
|
||||
|
||||
//Short circuit out if we detect we are attempting to extract patients
|
||||
if (myResourceType.equalsIgnoreCase("Patient")) {
|
||||
return getExpandedPatientIterator();
|
||||
}
|
||||
|
||||
|
||||
//First lets expand the group so we get a list of all patient IDs of the group, and MDM-matched patient IDs of the group.
|
||||
Set<String> expandedMemberResourceIds = expandAllPatientPidsFromGroup();
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("Group/{} has been expanded to members:[{}]", myGroupId, String.join(",", expandedMemberResourceIds));
|
||||
}
|
||||
|
||||
//Next, let's search for the target resources, with their correct patient references, chunked.
|
||||
//The results will be jammed into myReadPids
|
||||
Set<ResourcePersistentId> myExpandedMemberPids = new HashSet<>();
|
||||
QueryChunker<String> queryChunker = new QueryChunker<>();
|
||||
queryChunker.chunk(new ArrayList<>(expandedMemberResourceIds), QUERY_CHUNK_SIZE, (idChunk) -> {
|
||||
queryResourceTypeWithReferencesToPatients(myExpandedMemberPids, idChunk);
|
||||
});
|
||||
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("Resource PIDs to be Bulk Exported: {}", myExpandedMemberPids);
|
||||
}
|
||||
return myExpandedMemberPids.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are exporting a group during group export, we intentionally do not expand to other groups the member might be part of.
|
||||
* This code short-circuits the standard "expand group into its members then check the search compartment" logic.
|
||||
*
|
||||
* @return An iterator containing a single Group ID.
|
||||
*/
|
||||
private Iterator<ResourcePersistentId> getSingletonGroupIterator() {
|
||||
String stringedId = new IdDt(myGroupId).getIdPart();
|
||||
ResourcePersistentId groupId = myIdHelperService.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), myResourceType, stringedId);
|
||||
return Collections.singletonList(groupId).iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* In case we are doing a Group Bulk Export and resourceType `Patient` is requested, we can just return the group members,
|
||||
* possibly expanded by MDM, and don't have to go and fetch other resource DAOs.
|
||||
*/
|
||||
private Iterator<ResourcePersistentId> getExpandedPatientIterator() {
|
||||
List<String> members = getMembers();
|
||||
List<IIdType> ids = members.stream().map(member -> new IdDt("Patient/" + member)).collect(Collectors.toList());
|
||||
List<Long> pidsOrThrowException =myJpaIdHelperService.getPidsOrThrowException(ids);
|
||||
Set<Long> patientPidsToExport = new HashSet<>(pidsOrThrowException);
|
||||
|
||||
if (myMdmEnabled) {
|
||||
SystemRequestDetails srd = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), srd);
|
||||
Long pidOrNull = myJpaIdHelperService.getPidOrNull(group);
|
||||
List<IMdmLinkDao.MdmPidTuple> goldenPidSourcePidTuple = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH);
|
||||
goldenPidSourcePidTuple.forEach(tuple -> {
|
||||
patientPidsToExport.add(tuple.getGoldenPid());
|
||||
patientPidsToExport.add(tuple.getSourcePid());
|
||||
});
|
||||
populateMdmResourceCache(goldenPidSourcePidTuple);
|
||||
}
|
||||
List<ResourcePersistentId> resourcePersistentIds = patientPidsToExport
|
||||
.stream()
|
||||
.map(ResourcePersistentId::new)
|
||||
.collect(Collectors.toList());
|
||||
return resourcePersistentIds.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param thePidTuples
|
||||
*/
|
||||
private void populateMdmResourceCache(List<IMdmLinkDao.MdmPidTuple> thePidTuples) {
|
||||
if (myMdmExpansionCacheSvc.hasBeenPopulated()) {
|
||||
return;
|
||||
}
|
||||
//First, convert this zipped set of tuples to a map of
|
||||
//{
|
||||
// patient/gold-1 -> [patient/1, patient/2]
|
||||
// patient/gold-2 -> [patient/3, patient/4]
|
||||
//}
|
||||
Map<Long, Set<Long>> goldenResourceToSourcePidMap = new HashMap<>();
|
||||
extract(thePidTuples, goldenResourceToSourcePidMap);
|
||||
|
||||
//Next, lets convert it to an inverted index for fast lookup
|
||||
// {
|
||||
// patient/1 -> patient/gold-1
|
||||
// patient/2 -> patient/gold-1
|
||||
// patient/3 -> patient/gold-2
|
||||
// patient/4 -> patient/gold-2
|
||||
// }
|
||||
Map<String, String> sourceResourceIdToGoldenResourceIdMap = new HashMap<>();
|
||||
goldenResourceToSourcePidMap.forEach((key, value) -> {
|
||||
String goldenResourceId = myIdHelperService.translatePidIdToForcedIdWithCache(new ResourcePersistentId(key)).orElse(key.toString());
|
||||
Map<Long, Optional<String>> pidsToForcedIds = myIdHelperService.translatePidsToForcedIds(value);
|
||||
|
||||
Set<String> sourceResourceIds = pidsToForcedIds.entrySet().stream()
|
||||
.map(ent -> ent.getValue().isPresent() ? ent.getValue().get() : ent.getKey().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
sourceResourceIds
|
||||
.forEach(sourceResourceId -> sourceResourceIdToGoldenResourceIdMap.put(sourceResourceId, goldenResourceId));
|
||||
});
|
||||
|
||||
//Now that we have built our cached expansion, store it.
|
||||
myMdmExpansionCacheSvc.setCacheContents(sourceResourceIdToGoldenResourceIdMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the local myGroupId, read this group, and find all members' patient references.
|
||||
* @return A list of strings representing the Patient IDs of the members (e.g. ["P1", "P2", "P3"]
|
||||
*/
|
||||
private List<String> getMembers() {
|
||||
SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails);
|
||||
List<IPrimitiveType> evaluate = myContext.newFhirPath().evaluate(group, "member.entity.reference", IPrimitiveType.class);
|
||||
return evaluate.stream().map(IPrimitiveType::getValueAsString).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the local myGroupId, perform an expansion to retrieve all resource IDs of member patients.
|
||||
* if myMdmEnabled is set to true, we also reach out to the IMdmLinkDao to attempt to also expand it into matched
|
||||
* patients.
|
||||
*
|
||||
* @return a Set of Strings representing the resource IDs of all members of a group.
|
||||
*/
|
||||
private Set<String> expandAllPatientPidsFromGroup() {
|
||||
Set<String> expandedIds = new HashSet<>();
|
||||
SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails);
|
||||
Long pidOrNull = myJpaIdHelperService.getPidOrNull(group);
|
||||
|
||||
//Attempt to perform MDM Expansion of membership
|
||||
if (myMdmEnabled) {
|
||||
List<IMdmLinkDao.MdmPidTuple> goldenPidTargetPidTuples = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH);
|
||||
//Now lets translate these pids into resource IDs
|
||||
Set<Long> uniquePids = new HashSet<>();
|
||||
goldenPidTargetPidTuples.forEach(tuple -> {
|
||||
uniquePids.add(tuple.getGoldenPid());
|
||||
uniquePids.add(tuple.getSourcePid());
|
||||
});
|
||||
Map<Long, Optional<String>> pidToForcedIdMap = myIdHelperService.translatePidsToForcedIds(uniquePids);
|
||||
|
||||
Map<Long, Set<Long>> goldenResourceToSourcePidMap = new HashMap<>();
|
||||
extract(goldenPidTargetPidTuples, goldenResourceToSourcePidMap);
|
||||
populateMdmResourceCache(goldenPidTargetPidTuples);
|
||||
|
||||
//If the result of the translation is an empty optional, it means there is no forced id, and we can use the PID as the resource ID.
|
||||
Set<String> resolvedResourceIds = pidToForcedIdMap.entrySet().stream()
|
||||
.map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
expandedIds.addAll(resolvedResourceIds);
|
||||
}
|
||||
|
||||
//Now manually add the members of the group (its possible even with mdm expansion that some members dont have MDM matches,
|
||||
//so would be otherwise skipped
|
||||
expandedIds.addAll(getMembers());
|
||||
|
||||
return expandedIds;
|
||||
}
|
||||
|
||||
private void extract(List<IMdmLinkDao.MdmPidTuple> theGoldenPidTargetPidTuples, Map<Long, Set<Long>> theGoldenResourceToSourcePidMap) {
|
||||
for (IMdmLinkDao.MdmPidTuple goldenPidTargetPidTuple : theGoldenPidTargetPidTuples) {
|
||||
Long goldenPid = goldenPidTargetPidTuple.getGoldenPid();
|
||||
Long sourcePid = goldenPidTargetPidTuple.getSourcePid();
|
||||
theGoldenResourceToSourcePidMap.computeIfAbsent(goldenPid, key -> new HashSet<>()).add(sourcePid);
|
||||
}
|
||||
}
|
||||
|
||||
private void queryResourceTypeWithReferencesToPatients(Set<ResourcePersistentId> myReadPids, List<String> idChunk) {
|
||||
|
||||
//Build SP map
|
||||
//First, inject the _typeFilters and _since from the export job
|
||||
List<SearchParameterMap> expandedSpMaps = createSearchParameterMapsForResourceType();
|
||||
for (SearchParameterMap expandedSpMap: expandedSpMaps) {
|
||||
|
||||
//Since we are in a bulk job, we have to ensure the user didn't jam in a patient search param, since we need to manually set that.
|
||||
validateSearchParameters(expandedSpMap);
|
||||
|
||||
// Fetch and cache a search builder for this resource type
|
||||
ISearchBuilder searchBuilder = getSearchBuilderForLocalResourceType();
|
||||
|
||||
// Now, further filter the query with patient references defined by the chunk of IDs we have.
|
||||
if (PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(myResourceType)) {
|
||||
filterSearchByHasParam(idChunk, expandedSpMap);
|
||||
} else {
|
||||
filterSearchByResourceIds(idChunk, expandedSpMap);
|
||||
}
|
||||
|
||||
//Execute query and all found pids to our local iterator.
|
||||
IResultIterator resultIterator = searchBuilder.createQuery(expandedSpMap, new SearchRuntimeDetails(null, myJobUUID), null, RequestPartitionId.allPartitions());
|
||||
while (resultIterator.hasNext()) {
|
||||
myReadPids.add(resultIterator.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param idChunk
|
||||
* @param expandedSpMap
|
||||
*/
|
||||
private void filterSearchByHasParam(List<String> idChunk, SearchParameterMap expandedSpMap) {
|
||||
HasOrListParam hasOrListParam = new HasOrListParam();
|
||||
idChunk.stream().forEach(id -> hasOrListParam.addOr(buildHasParam(id)));
|
||||
expandedSpMap.add("_has", hasOrListParam);
|
||||
}
|
||||
|
||||
private HasParam buildHasParam(String theId) {
|
||||
if ("Practitioner".equalsIgnoreCase(myResourceType)) {
|
||||
return new HasParam("Patient", "general-practitioner", "_id", theId);
|
||||
} else if ("Organization".equalsIgnoreCase(myResourceType)) {
|
||||
return new HasParam("Patient", "organization", "_id", theId);
|
||||
} else {
|
||||
throw new IllegalArgumentException(Msg.code(2077) + " We can't handle forward references onto type " + myResourceType);
|
||||
}
|
||||
}
|
||||
|
||||
private void filterSearchByResourceIds(List<String> idChunk, SearchParameterMap expandedSpMap) {
|
||||
ReferenceOrListParam orList = new ReferenceOrListParam();
|
||||
idChunk.forEach(id -> orList.add(new ReferenceParam(id)));
|
||||
expandedSpMap.add(getPatientSearchParamForCurrentResourceType().getName(), orList);
|
||||
}
|
||||
|
||||
private void validateSearchParameters(SearchParameterMap expandedSpMap) {
|
||||
if (PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(myResourceType)) {
|
||||
return;
|
||||
} else {
|
||||
RuntimeSearchParam runtimeSearchParam = getPatientSearchParamForCurrentResourceType();
|
||||
if (expandedSpMap.get(runtimeSearchParam.getName()) != null) {
|
||||
throw new IllegalArgumentException(Msg.code(792) + String.format("Group Bulk Export manually modifies the Search Parameter called [%s], so you may not include this search parameter in your _typeFilter!", runtimeSearchParam.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.batch.log.Logs;
|
||||
import ca.uhn.fhir.jpa.dao.IResultIterator;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.batch.item.ItemReader;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bulk Item reader for the Patient Bulk Export job.
|
||||
* Instead of performing a normal query on the resource type using type filters, we instead
|
||||
*
|
||||
* 1. Determine the resourcetype
|
||||
* 2. Search for anything that has `patient-compartment-search-param:missing=false`
|
||||
*/
|
||||
public class PatientBulkItemReader extends BaseJpaBulkItemReader implements ItemReader<List<ResourcePersistentId>> {
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
|
||||
|
||||
|
||||
private RuntimeSearchParam validateSearchParameters(SearchParameterMap expandedSpMap) {
|
||||
RuntimeSearchParam runtimeSearchParam = getPatientSearchParamForCurrentResourceType();
|
||||
if (expandedSpMap.get(runtimeSearchParam.getName()) != null) {
|
||||
throw new IllegalArgumentException(Msg.code(796) + String.format("Patient Bulk Export manually modifies the Search Parameter called [%s], so you may not include this search parameter in your _typeFilter!", runtimeSearchParam.getName()));
|
||||
}
|
||||
return runtimeSearchParam;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterator<ResourcePersistentId> getResourcePidIterator() {
|
||||
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
|
||||
String errorMessage = "You attempted to start a Patient Bulk Export, but the system has `Index Missing Fields` disabled. It must be enabled for Patient Bulk Export";
|
||||
ourLog.error(errorMessage);
|
||||
throw new IllegalStateException(Msg.code(797) + errorMessage);
|
||||
}
|
||||
|
||||
List<ResourcePersistentId> myReadPids = new ArrayList<>();
|
||||
|
||||
//use _typeFilter and _since and all those fancy bits and bobs to generate our basic SP map.
|
||||
List<SearchParameterMap> maps = createSearchParameterMapsForResourceType();
|
||||
|
||||
String patientSearchParam = getPatientSearchParamForCurrentResourceType().getName();
|
||||
for (SearchParameterMap map: maps) {
|
||||
//Ensure users did not monkey with the patient compartment search parameter.
|
||||
validateSearchParameters(map);
|
||||
|
||||
//Skip adding the parameter querying for patient= if we are in fact querying the patient resource type.
|
||||
if (!myResourceType.equalsIgnoreCase("Patient")) {
|
||||
map.add(patientSearchParam, new ReferenceParam().setMissing(false));
|
||||
}
|
||||
|
||||
ourLog.debug("About to execute query {}", map.toNormalizedQueryString(myContext));
|
||||
ISearchBuilder sb = getSearchBuilderForLocalResourceType();
|
||||
IResultIterator myResultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, myJobUUID), null, RequestPartitionId.allPartitions());
|
||||
|
||||
while (myResultIterator.hasNext()) {
|
||||
myReadPids.add(myResultIterator.next());
|
||||
}
|
||||
}
|
||||
return myReadPids.iterator();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ResourceToFileWriter extends BaseResourceToFileWriter {
|
||||
@Autowired
|
||||
private BulkExportDaoSvc myBulkExportDaoSvc;
|
||||
|
||||
public ResourceToFileWriter(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
|
||||
super(theFhirContext, theDaoRegistry);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myBinaryDao = getBinaryDao();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<IIdType> flushToFiles() {
|
||||
if (myOutputStream.size() > 0) {
|
||||
IIdType createdId = createBinaryFromOutputStream();
|
||||
BulkExportCollectionFileEntity file = new BulkExportCollectionFileEntity();
|
||||
file.setResource(createdId.getIdPart());
|
||||
|
||||
myBulkExportDaoSvc.addFileToCollectionWithId(myBulkExportCollectionEntityId, file);
|
||||
|
||||
myOutputStream.reset();
|
||||
|
||||
return Optional.of(createdId);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private IFhirResourceDao<IBaseBinary> getBinaryDao() {
|
||||
return myDaoRegistry.getResourceDao("Binary");
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.job;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.batch.core.partition.support.Partitioner;
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
public class ResourceTypePartitioner implements Partitioner {
|
||||
private static final Logger ourLog = getLogger(ResourceTypePartitioner.class);
|
||||
|
||||
|
||||
@Value("#{jobExecutionContext['" + BatchConstants.JOB_UUID_PARAMETER+ "']}")
|
||||
private String myJobUUID;
|
||||
|
||||
@Autowired
|
||||
private BulkExportDaoSvc myBulkExportDaoSvc;
|
||||
|
||||
@Override
|
||||
public Map<String, ExecutionContext> partition(int gridSize) {
|
||||
Map<String, ExecutionContext> partitionContextMap = new HashMap<>();
|
||||
|
||||
Map<Long, String> idToResourceType = myBulkExportDaoSvc.getBulkJobCollectionIdToResourceTypeMap(myJobUUID);
|
||||
|
||||
idToResourceType.entrySet().stream()
|
||||
.forEach(entry -> {
|
||||
String resourceType = entry.getValue();
|
||||
Long collectionEntityId = entry.getKey();
|
||||
ourLog.debug("Creating a partition step for CollectionEntity: [{}] processing resource type [{}]", collectionEntityId, resourceType);
|
||||
|
||||
ExecutionContext context = new ExecutionContext();
|
||||
//The worker step needs to know what resource type it is looking for.
|
||||
context.putString(BatchConstants.JOB_EXECUTION_RESOURCE_TYPE, resourceType);
|
||||
|
||||
// The worker step needs to know which parent job it is processing for, and which collection entity it will be
|
||||
// attaching its results to.
|
||||
context.putString(BatchConstants.JOB_UUID_PARAMETER, myJobUUID);
|
||||
context.putLong(BatchConstants.JOB_COLLECTION_ENTITY_ID, collectionEntityId);
|
||||
|
||||
// Name the partition based on the resource type
|
||||
partitionContextMap.put(resourceType, context);
|
||||
});
|
||||
|
||||
|
||||
return partitionContextMap;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -67,8 +67,6 @@ import static org.slf4j.LoggerFactory.getLogger;
|
|||
public class BulkDataExportJobSchedulingHelperImpl implements IBulkDataExportJobSchedulingHelper {
|
||||
private static final Logger ourLog = getLogger(BulkDataExportJobSchedulingHelperImpl.class);
|
||||
|
||||
private static final Long READ_CHUNK_SIZE = 10L;
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
|
@ -88,18 +86,6 @@ public class BulkDataExportJobSchedulingHelperImpl implements IBulkDataExportJob
|
|||
@Autowired
|
||||
private ISchedulerService mySchedulerService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier(BatchConstants.BULK_EXPORT_JOB_NAME)
|
||||
private org.springframework.batch.core.Job myBulkExportJob;
|
||||
|
||||
@Autowired
|
||||
@Qualifier(BatchConstants.GROUP_BULK_EXPORT_JOB_NAME)
|
||||
private org.springframework.batch.core.Job myGroupBulkExportJob;
|
||||
|
||||
@Autowired
|
||||
@Qualifier(BatchConstants.PATIENT_BULK_EXPORT_JOB_NAME)
|
||||
private org.springframework.batch.core.Job myPatientBulkExportJob;
|
||||
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
|
||||
|
@ -109,70 +95,20 @@ public class BulkDataExportJobSchedulingHelperImpl implements IBulkDataExportJob
|
|||
@Autowired
|
||||
private FhirContext myContext;
|
||||
|
||||
@Autowired
|
||||
private BulkExportHelperService myBulkExportHelperSvc;
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myTxTemplate = new TransactionTemplate(myTxManager);
|
||||
|
||||
// job to cleanup unneeded BulkExportJobEntities that are persisted, but unwanted
|
||||
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
|
||||
jobDetail.setId(Job.class.getName());
|
||||
jobDetail.setJobClass(Job.class);
|
||||
mySchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail);
|
||||
|
||||
jobDetail = new ScheduledJobDefinition();
|
||||
jobDetail.setId(PurgeExpiredFilesJob.class.getName());
|
||||
jobDetail.setJobClass(PurgeExpiredFilesJob.class);
|
||||
mySchedulerService.scheduleClusteredJob(DateUtils.MILLIS_PER_HOUR, jobDetail);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the scheduler to run a pass of the
|
||||
* generator
|
||||
*/
|
||||
@Transactional(value = Transactional.TxType.NEVER)
|
||||
@Override
|
||||
public synchronized void startSubmittedJobs() {
|
||||
if (!myDaoConfig.isEnableTaskBulkExportJobExecution()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<BulkExportJobEntity> jobToProcessOpt = myTxTemplate.execute(t -> {
|
||||
Pageable page = PageRequest.of(0, 1);
|
||||
Slice<BulkExportJobEntity> submittedJobs = myBulkExportJobDao.findByStatus(page, BulkExportJobStatusEnum.SUBMITTED);
|
||||
if (submittedJobs.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(submittedJobs.getContent().get(0));
|
||||
});
|
||||
|
||||
if (!jobToProcessOpt.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BulkExportJobEntity bulkExportJobEntity = jobToProcessOpt.get();
|
||||
|
||||
String jobUuid = bulkExportJobEntity.getJobId();
|
||||
try {
|
||||
processJob(bulkExportJobEntity);
|
||||
} catch (Exception e) {
|
||||
ourLog.error("Failure while preparing bulk export extract", e);
|
||||
myTxTemplate.execute(t -> {
|
||||
Optional<BulkExportJobEntity> submittedJobs = myBulkExportJobDao.findByJobId(jobUuid);
|
||||
if (submittedJobs.isPresent()) {
|
||||
BulkExportJobEntity jobEntity = submittedJobs.get();
|
||||
jobEntity.setStatus(BulkExportJobStatusEnum.ERROR);
|
||||
jobEntity.setStatusMessage(e.getMessage());
|
||||
myBulkExportJobDao.save(jobEntity);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.NEVER)
|
||||
public synchronized void cancelAndPurgeAllJobs() {
|
||||
|
@ -208,18 +144,17 @@ public class BulkDataExportJobSchedulingHelperImpl implements IBulkDataExportJob
|
|||
});
|
||||
|
||||
if (jobToDelete.isPresent()) {
|
||||
|
||||
ourLog.info("Deleting bulk export job: {}", jobToDelete.get());
|
||||
|
||||
myTxTemplate.execute(t -> {
|
||||
|
||||
BulkExportJobEntity job = myBulkExportJobDao.getOne(jobToDelete.get().getId());
|
||||
for (BulkExportCollectionEntity nextCollection : job.getCollections()) {
|
||||
for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) {
|
||||
|
||||
ourLog.info("Purging bulk data file: {}", nextFile.getResourceId());
|
||||
getBinaryDao().delete(toId(nextFile.getResourceId()), new SystemRequestDetails());
|
||||
getBinaryDao().forceExpungeInExistingTransaction(toId(nextFile.getResourceId()), new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), new SystemRequestDetails());
|
||||
IIdType id = myBulkExportHelperSvc.toId(nextFile.getResourceId());
|
||||
getBinaryDao().delete(id, new SystemRequestDetails());
|
||||
getBinaryDao().forceExpungeInExistingTransaction(id, new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), new SystemRequestDetails());
|
||||
myBulkExportCollectionFileDao.deleteByPid(nextFile.getId());
|
||||
|
||||
}
|
||||
|
@ -241,71 +176,6 @@ public class BulkDataExportJobSchedulingHelperImpl implements IBulkDataExportJob
|
|||
return myDaoRegistry.getResourceDao("Binary");
|
||||
}
|
||||
|
||||
private IIdType toId(String theResourceId) {
|
||||
IIdType retVal = myContext.getVersion().newIdType();
|
||||
retVal.setValue(theResourceId);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private void processJob(BulkExportJobEntity theBulkExportJobEntity) {
|
||||
String theJobUuid = theBulkExportJobEntity.getJobId();
|
||||
JobParametersBuilder parameters = new JobParametersBuilder()
|
||||
.addString(BatchConstants.JOB_UUID_PARAMETER, theJobUuid)
|
||||
.addLong(BatchConstants.READ_CHUNK_PARAMETER, READ_CHUNK_SIZE);
|
||||
|
||||
ourLog.info("Submitting bulk export job {} to job scheduler", theJobUuid);
|
||||
|
||||
try {
|
||||
if (isGroupBulkJob(theBulkExportJobEntity)) {
|
||||
enhanceBulkParametersWithGroupParameters(theBulkExportJobEntity, parameters);
|
||||
myJobSubmitter.runJob(myGroupBulkExportJob, parameters.toJobParameters());
|
||||
} else if (isPatientBulkJob(theBulkExportJobEntity)) {
|
||||
myJobSubmitter.runJob(myPatientBulkExportJob, parameters.toJobParameters());
|
||||
} else {
|
||||
myJobSubmitter.runJob(myBulkExportJob, parameters.toJobParameters());
|
||||
}
|
||||
} catch (JobParametersInvalidException theE) {
|
||||
ourLog.error("Unable to start job with UUID: {}, the parameters are invalid. {}", theJobUuid, theE.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getQueryParameterIfPresent(String theRequestString, String theParameter) {
|
||||
Map<String, String[]> stringMap = UrlUtil.parseQueryString(theRequestString);
|
||||
if (stringMap != null) {
|
||||
String[] strings = stringMap.get(theParameter);
|
||||
if (strings != null) {
|
||||
return String.join(",", strings);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private boolean isPatientBulkJob(BulkExportJobEntity theBulkExportJobEntity) {
|
||||
return theBulkExportJobEntity.getRequest().startsWith("/Patient/");
|
||||
}
|
||||
|
||||
private boolean isGroupBulkJob(BulkExportJobEntity theBulkExportJobEntity) {
|
||||
return theBulkExportJobEntity.getRequest().startsWith("/Group/");
|
||||
}
|
||||
|
||||
private void enhanceBulkParametersWithGroupParameters(BulkExportJobEntity theBulkExportJobEntity, JobParametersBuilder theParameters) {
|
||||
String theGroupId = getQueryParameterIfPresent(theBulkExportJobEntity.getRequest(), JpaConstants.PARAM_EXPORT_GROUP_ID);
|
||||
String expandMdm = getQueryParameterIfPresent(theBulkExportJobEntity.getRequest(), JpaConstants.PARAM_EXPORT_MDM);
|
||||
theParameters.addString(BatchConstants.GROUP_ID_PARAMETER, theGroupId);
|
||||
theParameters.addString(BatchConstants.EXPAND_MDM_PARAMETER, expandMdm);
|
||||
}
|
||||
|
||||
public static class Job implements HapiJob {
|
||||
@Autowired
|
||||
private IBulkDataExportJobSchedulingHelper myTarget;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext theContext) {
|
||||
myTarget.startSubmittedJobs();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PurgeExpiredFilesJob implements HapiJob {
|
||||
@Autowired
|
||||
private IBulkDataExportJobSchedulingHelper myTarget;
|
||||
|
|
|
@ -1,306 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.svc;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
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 ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.InstantType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions.ExportStyle.GROUP;
|
||||
import static ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions.ExportStyle.PATIENT;
|
||||
import static ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions.ExportStyle.SYSTEM;
|
||||
import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam;
|
||||
import static ca.uhn.fhir.util.UrlUtil.escapeUrlParams;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class BulkDataExportSvcImpl implements IBulkDataExportSvc {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportSvcImpl.class);
|
||||
private final int myReuseBulkExportForMillis = (int) (60 * DateUtils.MILLIS_PER_MINUTE);
|
||||
|
||||
@Autowired
|
||||
private IBulkExportJobDao myBulkExportJobDao;
|
||||
@Autowired
|
||||
private IBulkExportCollectionDao myBulkExportCollectionDao;
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
private Set<String> myCompartmentResources;
|
||||
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@Deprecated
|
||||
public JobInfo submitJob(BulkDataExportOptions theBulkDataExportOptions) {
|
||||
return submitJob(theBulkDataExportOptions, true, null);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public JobInfo submitJob(BulkDataExportOptions theBulkDataExportOptions, Boolean useCache, RequestDetails theRequestDetails) {
|
||||
String outputFormat = Constants.CT_FHIR_NDJSON;
|
||||
if (isNotBlank(theBulkDataExportOptions.getOutputFormat())) {
|
||||
outputFormat = theBulkDataExportOptions.getOutputFormat();
|
||||
}
|
||||
if (!Constants.CTS_NDJSON.contains(outputFormat)) {
|
||||
throw new InvalidRequestException(Msg.code(786) + "Invalid output format: " + theBulkDataExportOptions.getOutputFormat());
|
||||
}
|
||||
|
||||
// Interceptor call: STORAGE_INITIATE_BULK_EXPORT
|
||||
HookParams params = new HookParams()
|
||||
.add(BulkDataExportOptions.class, theBulkDataExportOptions)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_INITIATE_BULK_EXPORT, params);
|
||||
|
||||
// TODO GGG KS can we encode BulkDataExportOptions as a JSON string as opposed to this request string. Feels like it would be a more extensible encoding...
|
||||
//Probably yes, but this will all need to be rebuilt when we remove this bridge entity
|
||||
StringBuilder requestBuilder = new StringBuilder();
|
||||
requestBuilder.append("/");
|
||||
|
||||
//Prefix the export url with Group/[id]/ or /Patient/ depending on what type of request it is.
|
||||
if (theBulkDataExportOptions.getExportStyle().equals(GROUP)) {
|
||||
requestBuilder.append(theBulkDataExportOptions.getGroupId().toVersionless()).append("/");
|
||||
} else if (theBulkDataExportOptions.getExportStyle().equals(PATIENT)) {
|
||||
requestBuilder.append("Patient/");
|
||||
}
|
||||
|
||||
requestBuilder.append(JpaConstants.OPERATION_EXPORT);
|
||||
requestBuilder.append("?").append(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT).append("=").append(escapeUrlParam(outputFormat));
|
||||
Set<String> resourceTypes = theBulkDataExportOptions.getResourceTypes();
|
||||
if (resourceTypes != null) {
|
||||
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_TYPE).append("=").append(String.join(",", escapeUrlParams(resourceTypes)));
|
||||
}
|
||||
Date since = theBulkDataExportOptions.getSince();
|
||||
if (since != null) {
|
||||
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_SINCE).append("=").append(new InstantType(since).setTimeZoneZulu(true).getValueAsString());
|
||||
}
|
||||
if (theBulkDataExportOptions.getFilters() != null && theBulkDataExportOptions.getFilters().size() > 0) {
|
||||
theBulkDataExportOptions.getFilters().stream()
|
||||
.forEach(filter -> requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_TYPE_FILTER).append("=").append(escapeUrlParam(filter)));
|
||||
}
|
||||
|
||||
if (theBulkDataExportOptions.getExportStyle().equals(GROUP)) {
|
||||
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_GROUP_ID).append("=").append(theBulkDataExportOptions.getGroupId().getValue());
|
||||
requestBuilder.append("&").append(JpaConstants.PARAM_EXPORT_MDM).append("=").append(theBulkDataExportOptions.isExpandMdm());
|
||||
}
|
||||
|
||||
String request = requestBuilder.toString();
|
||||
|
||||
//If we are using the cache, then attempt to retrieve a matching job based on the Request String, otherwise just make a new one.
|
||||
if (useCache) {
|
||||
Date cutoff = DateUtils.addMilliseconds(new Date(), -myReuseBulkExportForMillis);
|
||||
Pageable page = PageRequest.of(0, 10);
|
||||
Slice<BulkExportJobEntity> existing = myBulkExportJobDao.findExistingJob(page, request, cutoff, BulkExportJobStatusEnum.ERROR);
|
||||
if (!existing.isEmpty()) {
|
||||
return toSubmittedJobInfo(existing.iterator().next());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypes != null && resourceTypes.contains("Binary")) {
|
||||
String msg = myContext.getLocalizer().getMessage(BulkDataExportSvcImpl.class, "onlyBinarySelected");
|
||||
throw new InvalidRequestException(Msg.code(787) + msg);
|
||||
}
|
||||
|
||||
if (resourceTypes == null || resourceTypes.isEmpty()) {
|
||||
// This is probably not a useful default, but having the default be "download the whole
|
||||
// server" seems like a risky default too. We'll deal with that by having the default involve
|
||||
// only returning a small time span
|
||||
resourceTypes = getAllowedResourceTypesForBulkExportStyle(theBulkDataExportOptions.getExportStyle());
|
||||
if (since == null) {
|
||||
since = DateUtils.addDays(new Date(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
resourceTypes =
|
||||
resourceTypes
|
||||
.stream()
|
||||
.filter(t -> !"Binary".equals(t))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
BulkExportJobEntity jobEntity = new BulkExportJobEntity();
|
||||
jobEntity.setJobId(UUID.randomUUID().toString());
|
||||
jobEntity.setStatus(BulkExportJobStatusEnum.SUBMITTED);
|
||||
jobEntity.setSince(since);
|
||||
jobEntity.setCreated(new Date());
|
||||
jobEntity.setRequest(request);
|
||||
|
||||
// Validate types
|
||||
validateTypes(resourceTypes);
|
||||
validateTypeFilters(theBulkDataExportOptions.getFilters(), resourceTypes);
|
||||
|
||||
updateExpiry(jobEntity);
|
||||
myBulkExportJobDao.save(jobEntity);
|
||||
|
||||
for (String nextType : resourceTypes) {
|
||||
|
||||
BulkExportCollectionEntity collection = new BulkExportCollectionEntity();
|
||||
collection.setJob(jobEntity);
|
||||
collection.setResourceType(nextType);
|
||||
jobEntity.getCollections().add(collection);
|
||||
myBulkExportCollectionDao.save(collection);
|
||||
}
|
||||
|
||||
ourLog.info("Bulk export job submitted: {}", jobEntity.toString());
|
||||
|
||||
return toSubmittedJobInfo(jobEntity);
|
||||
}
|
||||
|
||||
public void validateTypes(Set<String> theResourceTypes) {
|
||||
for (String nextType : theResourceTypes) {
|
||||
if (!myDaoRegistry.isResourceTypeSupported(nextType)) {
|
||||
String msg = myContext.getLocalizer().getMessage(BulkDataExportSvcImpl.class, "unknownResourceType", nextType);
|
||||
throw new InvalidRequestException(Msg.code(788) + msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTypeFilters(Set<String> theTheFilters, Set<String> theResourceTypes) {
|
||||
if (theTheFilters != null) {
|
||||
for (String next : theTheFilters) {
|
||||
if (!next.contains("?")) {
|
||||
throw new InvalidRequestException(Msg.code(789) + "Invalid " + JpaConstants.PARAM_EXPORT_TYPE_FILTER + " value \"" + next + "\". Must be in the form [ResourceType]?[params]");
|
||||
}
|
||||
String resourceType = next.substring(0, next.indexOf("?"));
|
||||
if (!theResourceTypes.contains(resourceType)) {
|
||||
throw new InvalidRequestException(Msg.code(790) + "Invalid " + JpaConstants.PARAM_EXPORT_TYPE_FILTER + " value \"" + next + "\". Resource type does not appear in " + JpaConstants.PARAM_EXPORT_TYPE+ " list");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JobInfo toSubmittedJobInfo(BulkExportJobEntity theJob) {
|
||||
return new JobInfo()
|
||||
.setJobId(theJob.getJobId())
|
||||
.setStatus(theJob.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* If the retention period is set to <= 0, set it to null, which prevents it from getting expired, otherwise, set
|
||||
* the retention period.
|
||||
*
|
||||
* @param theJob the job to update the expiry for.
|
||||
*/
|
||||
private void updateExpiry(BulkExportJobEntity theJob) {
|
||||
if (myDaoConfig.getBulkExportFileRetentionPeriodHours() > 0) {
|
||||
theJob.setExpiry(DateUtils.addHours(new Date(), myDaoConfig.getBulkExportFileRetentionPeriodHours()));
|
||||
} else {
|
||||
theJob.setExpiry(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public JobInfo getJobInfoOrThrowResourceNotFound(String theJobId) {
|
||||
BulkExportJobEntity job = myBulkExportJobDao
|
||||
.findByJobId(theJobId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(theJobId));
|
||||
|
||||
JobInfo retVal = new JobInfo();
|
||||
retVal.setJobId(theJobId);
|
||||
retVal.setStatus(job.getStatus());
|
||||
retVal.setStatus(job.getStatus());
|
||||
retVal.setStatusTime(job.getStatusTime());
|
||||
retVal.setStatusMessage(job.getStatusMessage());
|
||||
retVal.setRequest(job.getRequest());
|
||||
|
||||
if (job.getStatus() == BulkExportJobStatusEnum.COMPLETE) {
|
||||
for (BulkExportCollectionEntity nextCollection : job.getCollections()) {
|
||||
for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) {
|
||||
retVal.addFile()
|
||||
.setResourceType(nextCollection.getResourceType())
|
||||
.setResourceId(toQualifiedBinaryId(nextFile.getResourceId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPatientCompartmentResources() {
|
||||
if (myCompartmentResources == null) {
|
||||
myCompartmentResources = myContext.getResourceTypes().stream()
|
||||
.filter(resType -> SearchParameterUtil.isResourceTypeInPatientCompartment(myContext, resType))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
return myCompartmentResources;
|
||||
}
|
||||
|
||||
public Set<String> getAllowedResourceTypesForBulkExportStyle(BulkDataExportOptions.ExportStyle theExportStyle) {
|
||||
if (theExportStyle.equals(SYSTEM)) {
|
||||
return myContext.getResourceTypes();
|
||||
} else if (theExportStyle.equals(GROUP) || theExportStyle.equals(PATIENT)) {
|
||||
return getPatientCompartmentResources();
|
||||
} else {
|
||||
throw new IllegalArgumentException(Msg.code(791) + String.format("HAPI FHIR does not recognize a Bulk Export request of type: %s", theExportStyle));
|
||||
}
|
||||
}
|
||||
|
||||
private IIdType toQualifiedBinaryId(String theIdPart) {
|
||||
IIdType retVal = myContext.getVersion().newIdType();
|
||||
retVal.setParts(null, "Binary", theIdPart, null);
|
||||
|
||||
return retVal;
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.svc;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity;
|
||||
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
@Service
|
||||
public class BulkExportDaoSvc {
|
||||
private static final Logger ourLog = getLogger(BulkExportDaoSvc.class);
|
||||
@Autowired
|
||||
IBulkExportJobDao myBulkExportJobDao;
|
||||
@Autowired
|
||||
IBulkExportCollectionDao myBulkExportCollectionDao;
|
||||
@Autowired
|
||||
IBulkExportCollectionFileDao myBulkExportCollectionFileDao;
|
||||
|
||||
@Transactional
|
||||
public void addFileToCollectionWithId(Long theCollectionEntityId, BulkExportCollectionFileEntity theFile) {
|
||||
Optional<BulkExportCollectionEntity> byId = myBulkExportCollectionDao.findById(theCollectionEntityId);
|
||||
if (byId.isPresent()) {
|
||||
BulkExportCollectionEntity exportCollectionEntity = byId.get();
|
||||
theFile.setCollection(exportCollectionEntity);
|
||||
exportCollectionEntity.getFiles().add(theFile);
|
||||
myBulkExportCollectionFileDao.saveAndFlush(theFile);
|
||||
myBulkExportCollectionDao.saveAndFlush(exportCollectionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<Long, String> getBulkJobCollectionIdToResourceTypeMap(String theJobUUID) {
|
||||
BulkExportJobEntity bulkExportJobEntity = loadJob(theJobUUID);
|
||||
Collection<BulkExportCollectionEntity> collections = bulkExportJobEntity.getCollections();
|
||||
return collections.stream()
|
||||
.collect(Collectors.toMap(
|
||||
BulkExportCollectionEntity::getId,
|
||||
BulkExportCollectionEntity::getResourceType
|
||||
));
|
||||
}
|
||||
|
||||
private BulkExportJobEntity loadJob(String theJobUUID) {
|
||||
Optional<BulkExportJobEntity> jobOpt = myBulkExportJobDao.findByJobId(theJobUUID);
|
||||
if (!jobOpt.isPresent()) {
|
||||
ourLog.warn("Job with UUID {} appears to be deleted", theJobUUID);
|
||||
return null;
|
||||
}
|
||||
return jobOpt.get();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setJobToStatus(String theJobUUID, BulkExportJobStatusEnum theStatus) {
|
||||
setJobToStatus(theJobUUID, theStatus, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setJobToStatus(String theJobUUID, BulkExportJobStatusEnum theStatus, String theStatusMessage) {
|
||||
Optional<BulkExportJobEntity> oJob = myBulkExportJobDao.findByJobId(theJobUUID);
|
||||
if (!oJob.isPresent()) {
|
||||
ourLog.error("Job with UUID {} doesn't exist!", theJobUUID);
|
||||
return;
|
||||
}
|
||||
|
||||
ourLog.info("Setting job with UUID {} to {}", theJobUUID, theStatus);
|
||||
BulkExportJobEntity bulkExportJobEntity = oJob.get();
|
||||
bulkExportJobEntity.setStatus(theStatus);
|
||||
bulkExportJobEntity.setStatusMessage(theStatusMessage);
|
||||
myBulkExportJobDao.save(bulkExportJobEntity);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.svc;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.fhirpath.IFhirPath;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkExportProcessor;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.ExportPIDIteratorParameters;
|
||||
import ca.uhn.fhir.jpa.dao.IResultIterator;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
|
||||
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IJpaIdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.util.QueryChunker;
|
||||
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.param.HasOrListParam;
|
||||
import ca.uhn.fhir.rest.param.HasParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.util.ExtensionUtil;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JpaBulkExportProcessor implements IBulkExportProcessor {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(JpaBulkExportProcessor.class);
|
||||
|
||||
public static final int QUERY_CHUNK_SIZE = 100;
|
||||
public static final List<String> PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES = List.of("Practitioner", "Organization");
|
||||
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
|
||||
@Autowired
|
||||
private BulkExportHelperService myBulkExportHelperSvc;
|
||||
|
||||
@Autowired
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Autowired
|
||||
protected SearchBuilderFactory mySearchBuilderFactory;
|
||||
|
||||
@Autowired
|
||||
private IIdHelperService myIdHelperService;
|
||||
|
||||
@Autowired
|
||||
private IMdmLinkDao myMdmLinkDao;
|
||||
|
||||
@Autowired
|
||||
private IJpaIdHelperService myJpaIdHelperService;
|
||||
|
||||
@Autowired
|
||||
private MdmExpansionCacheSvc myMdmExpansionCacheSvc;
|
||||
|
||||
private final HashMap<String, ISearchBuilder> myResourceTypeToSearchBuilder = new HashMap<>();
|
||||
|
||||
private final HashMap<String, String> myResourceTypeToFhirPath = new HashMap<>();
|
||||
|
||||
private IFhirPath myFhirPath;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Iterator<ResourcePersistentId> getResourcePidIterator(ExportPIDIteratorParameters theParams) {
|
||||
String resourceType = theParams.getResourceType();
|
||||
String jobId = theParams.getJobId();
|
||||
RuntimeResourceDefinition def = myContext.getResourceDefinition(resourceType);
|
||||
|
||||
Set<ResourcePersistentId> pids = new HashSet<>();
|
||||
|
||||
if (theParams.getExportStyle() == BulkDataExportOptions.ExportStyle.PATIENT) {
|
||||
// Patient
|
||||
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
|
||||
String errorMessage = "You attempted to start a Patient Bulk Export, but the system has `Index Missing Fields` disabled. It must be enabled for Patient Bulk Export";
|
||||
ourLog.error(errorMessage);
|
||||
throw new IllegalStateException(Msg.code(797) + errorMessage);
|
||||
}
|
||||
|
||||
List<SearchParameterMap> maps = myBulkExportHelperSvc.createSearchParameterMapsForResourceType(def, theParams);
|
||||
String patientSearchParam = getPatientSearchParamForCurrentResourceType(theParams.getResourceType()).getName();
|
||||
|
||||
for (SearchParameterMap map : maps) {
|
||||
//Ensure users did not monkey with the patient compartment search parameter.
|
||||
validateSearchParametersForPatient(map, theParams);
|
||||
|
||||
ISearchBuilder searchBuilder = getSearchBuilderForLocalResourceType(theParams);
|
||||
|
||||
if (!resourceType.equalsIgnoreCase("Patient")) {
|
||||
map.add(patientSearchParam, new ReferenceParam().setMissing(false));
|
||||
}
|
||||
|
||||
IResultIterator resultIterator = searchBuilder.createQuery(map,
|
||||
new SearchRuntimeDetails(null, jobId),
|
||||
null,
|
||||
RequestPartitionId.allPartitions());
|
||||
while (resultIterator.hasNext()) {
|
||||
pids.add(resultIterator.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (theParams.getExportStyle() == BulkDataExportOptions.ExportStyle.GROUP) {
|
||||
// Group
|
||||
if (resourceType.equalsIgnoreCase("Patient")) {
|
||||
return getExpandedPatientIterator(theParams);
|
||||
}
|
||||
|
||||
Set<String> expandedMemberResourceIds = expandAllPatientPidsFromGroup(theParams);
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("Group/{} has been expanded to members:[{}]", theParams, String.join(",", expandedMemberResourceIds));
|
||||
}
|
||||
|
||||
//Next, let's search for the target resources, with their correct patient references, chunked.
|
||||
//The results will be jammed into myReadPids
|
||||
QueryChunker<String> queryChunker = new QueryChunker<>();
|
||||
queryChunker.chunk(new ArrayList<>(expandedMemberResourceIds), QUERY_CHUNK_SIZE, (idChunk) -> {
|
||||
queryResourceTypeWithReferencesToPatients(pids, idChunk, theParams, def);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// System
|
||||
List<SearchParameterMap> maps = myBulkExportHelperSvc.createSearchParameterMapsForResourceType(def, theParams);
|
||||
ISearchBuilder searchBuilder = getSearchBuilderForLocalResourceType(theParams);
|
||||
|
||||
for (SearchParameterMap map : maps) {
|
||||
// requires a transaction
|
||||
IResultIterator resultIterator = searchBuilder.createQuery(map,
|
||||
new SearchRuntimeDetails(null, jobId),
|
||||
null,
|
||||
RequestPartitionId.allPartitions());
|
||||
while (resultIterator.hasNext()) {
|
||||
pids.add(resultIterator.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pids.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and cache an ISearchBuilder for the given resource type this partition is responsible for.
|
||||
*/
|
||||
protected ISearchBuilder getSearchBuilderForLocalResourceType(ExportPIDIteratorParameters theParams) {
|
||||
String resourceType = theParams.getResourceType();
|
||||
if (!myResourceTypeToSearchBuilder.containsKey(resourceType)) {
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
RuntimeResourceDefinition def = myContext.getResourceDefinition(resourceType);
|
||||
Class<? extends IBaseResource> nextTypeClass = def.getImplementingClass();
|
||||
ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(dao, resourceType, nextTypeClass);
|
||||
myResourceTypeToSearchBuilder.put(resourceType, sb);
|
||||
}
|
||||
return myResourceTypeToSearchBuilder.get(resourceType);
|
||||
}
|
||||
|
||||
protected RuntimeSearchParam getPatientSearchParamForCurrentResourceType(String theResourceType) {
|
||||
RuntimeSearchParam searchParam = null;
|
||||
Optional<RuntimeSearchParam> onlyPatientSearchParamForResourceType = SearchParameterUtil.getOnlyPatientSearchParamForResourceType(myContext, theResourceType);
|
||||
if (onlyPatientSearchParamForResourceType.isPresent()) {
|
||||
searchParam = onlyPatientSearchParamForResourceType.get();
|
||||
}
|
||||
return searchParam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expandMdmResources(List<IBaseResource> theResources) {
|
||||
for (IBaseResource resource : theResources) {
|
||||
if (!PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(resource.fhirType())) {
|
||||
annotateBackwardsReferences(resource);
|
||||
}
|
||||
}
|
||||
|
||||
// is this necessary?
|
||||
myResourceTypeToFhirPath.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* For Patient
|
||||
**/
|
||||
|
||||
private RuntimeSearchParam validateSearchParametersForPatient(SearchParameterMap expandedSpMap, ExportPIDIteratorParameters theParams) {
|
||||
RuntimeSearchParam runtimeSearchParam = getPatientSearchParamForCurrentResourceType(theParams.getResourceType());
|
||||
if (expandedSpMap.get(runtimeSearchParam.getName()) != null) {
|
||||
throw new IllegalArgumentException(Msg.code(796) + String.format("Patient Bulk Export manually modifies the Search Parameter called [%s], so you may not include this search parameter in your _typeFilter!", runtimeSearchParam.getName()));
|
||||
}
|
||||
return runtimeSearchParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* for group exports
|
||||
**/
|
||||
|
||||
private void validateSearchParametersForGroup(SearchParameterMap expandedSpMap, String theResourceType) {
|
||||
// we only validate for certain types
|
||||
if (!PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(theResourceType)) {
|
||||
RuntimeSearchParam runtimeSearchParam = getPatientSearchParamForCurrentResourceType(theResourceType);
|
||||
if (expandedSpMap.get(runtimeSearchParam.getName()) != null) {
|
||||
throw new IllegalArgumentException(Msg.code(792) + String.format("Group Bulk Export manually modifies the Search Parameter called [%s], so you may not include this search parameter in your _typeFilter!", runtimeSearchParam.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case we are doing a Group Bulk Export and resourceType `Patient` is requested, we can just return the group members,
|
||||
* possibly expanded by MDM, and don't have to go and fetch other resource DAOs.
|
||||
*/
|
||||
private Iterator<ResourcePersistentId> getExpandedPatientIterator(ExportPIDIteratorParameters theParameters) {
|
||||
List<String> members = getMembers(theParameters.getGroupId());
|
||||
List<IIdType> ids = members.stream().map(member -> new IdDt("Patient/" + member)).collect(Collectors.toList());
|
||||
List<Long> pidsOrThrowException = myJpaIdHelperService.getPidsOrThrowException(ids);
|
||||
Set<Long> patientPidsToExport = new HashSet<>(pidsOrThrowException);
|
||||
|
||||
if (theParameters.isExpandMdm()) {
|
||||
SystemRequestDetails srd = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(theParameters.getGroupId()), srd);
|
||||
Long pidOrNull = myJpaIdHelperService.getPidOrNull(group);
|
||||
List<IMdmLinkDao.MdmPidTuple> goldenPidSourcePidTuple = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH);
|
||||
goldenPidSourcePidTuple.forEach(tuple -> {
|
||||
patientPidsToExport.add(tuple.getGoldenPid());
|
||||
patientPidsToExport.add(tuple.getSourcePid());
|
||||
});
|
||||
populateMdmResourceCache(goldenPidSourcePidTuple);
|
||||
}
|
||||
List<ResourcePersistentId> resourcePersistentIds = patientPidsToExport
|
||||
.stream()
|
||||
.map(ResourcePersistentId::new)
|
||||
.collect(Collectors.toList());
|
||||
return resourcePersistentIds.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the local myGroupId, read this group, and find all members' patient references.
|
||||
*
|
||||
* @return A list of strings representing the Patient IDs of the members (e.g. ["P1", "P2", "P3"]
|
||||
*/
|
||||
private List<String> getMembers(String theGroupId) {
|
||||
SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(theGroupId), requestDetails);
|
||||
List<IPrimitiveType> evaluate = myContext.newFhirPath().evaluate(group, "member.entity.reference", IPrimitiveType.class);
|
||||
return evaluate.stream().map(IPrimitiveType::getValueAsString).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param thePidTuples
|
||||
*/
|
||||
private void populateMdmResourceCache(List<IMdmLinkDao.MdmPidTuple> thePidTuples) {
|
||||
if (myMdmExpansionCacheSvc.hasBeenPopulated()) {
|
||||
return;
|
||||
}
|
||||
//First, convert this zipped set of tuples to a map of
|
||||
//{
|
||||
// patient/gold-1 -> [patient/1, patient/2]
|
||||
// patient/gold-2 -> [patient/3, patient/4]
|
||||
//}
|
||||
Map<Long, Set<Long>> goldenResourceToSourcePidMap = new HashMap<>();
|
||||
extract(thePidTuples, goldenResourceToSourcePidMap);
|
||||
|
||||
//Next, lets convert it to an inverted index for fast lookup
|
||||
// {
|
||||
// patient/1 -> patient/gold-1
|
||||
// patient/2 -> patient/gold-1
|
||||
// patient/3 -> patient/gold-2
|
||||
// patient/4 -> patient/gold-2
|
||||
// }
|
||||
Map<String, String> sourceResourceIdToGoldenResourceIdMap = new HashMap<>();
|
||||
goldenResourceToSourcePidMap.forEach((key, value) -> {
|
||||
String goldenResourceId = myIdHelperService.translatePidIdToForcedIdWithCache(new ResourcePersistentId(key)).orElse(key.toString());
|
||||
Map<Long, Optional<String>> pidsToForcedIds = myIdHelperService.translatePidsToForcedIds(value);
|
||||
|
||||
Set<String> sourceResourceIds = pidsToForcedIds.entrySet().stream()
|
||||
.map(ent -> ent.getValue().isPresent() ? ent.getValue().get() : ent.getKey().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
sourceResourceIds
|
||||
.forEach(sourceResourceId -> sourceResourceIdToGoldenResourceIdMap.put(sourceResourceId, goldenResourceId));
|
||||
});
|
||||
|
||||
//Now that we have built our cached expansion, store it.
|
||||
myMdmExpansionCacheSvc.setCacheContents(sourceResourceIdToGoldenResourceIdMap);
|
||||
}
|
||||
|
||||
private void extract(List<IMdmLinkDao.MdmPidTuple> theGoldenPidTargetPidTuples, Map<Long, Set<Long>> theGoldenResourceToSourcePidMap) {
|
||||
for (IMdmLinkDao.MdmPidTuple goldenPidTargetPidTuple : theGoldenPidTargetPidTuples) {
|
||||
Long goldenPid = goldenPidTargetPidTuple.getGoldenPid();
|
||||
Long sourcePid = goldenPidTargetPidTuple.getSourcePid();
|
||||
theGoldenResourceToSourcePidMap.computeIfAbsent(goldenPid, key -> new HashSet<>()).add(sourcePid);
|
||||
}
|
||||
}
|
||||
|
||||
private void queryResourceTypeWithReferencesToPatients(Set<ResourcePersistentId> myReadPids,
|
||||
List<String> idChunk,
|
||||
ExportPIDIteratorParameters theParams,
|
||||
RuntimeResourceDefinition theDef) {
|
||||
//Build SP map
|
||||
//First, inject the _typeFilters and _since from the export job
|
||||
List<SearchParameterMap> expandedSpMaps = myBulkExportHelperSvc.createSearchParameterMapsForResourceType(theDef, theParams);
|
||||
for (SearchParameterMap expandedSpMap : expandedSpMaps) {
|
||||
|
||||
//Since we are in a bulk job, we have to ensure the user didn't jam in a patient search param, since we need to manually set that.
|
||||
validateSearchParametersForGroup(expandedSpMap, theParams.getResourceType());
|
||||
|
||||
// Fetch and cache a search builder for this resource type
|
||||
ISearchBuilder searchBuilder = getSearchBuilderForLocalResourceType(theParams);
|
||||
|
||||
// Now, further filter the query with patient references defined by the chunk of IDs we have.
|
||||
if (PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(theParams.getResourceType())) {
|
||||
filterSearchByHasParam(idChunk, expandedSpMap, theParams);
|
||||
} else {
|
||||
filterSearchByResourceIds(idChunk, expandedSpMap, theParams);
|
||||
}
|
||||
|
||||
//Execute query and all found pids to our local iterator.
|
||||
IResultIterator resultIterator = searchBuilder.createQuery(expandedSpMap,
|
||||
new SearchRuntimeDetails(null, theParams.getJobId()),
|
||||
null,
|
||||
RequestPartitionId.allPartitions());
|
||||
while (resultIterator.hasNext()) {
|
||||
myReadPids.add(resultIterator.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must not be called for resources types listed in PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES
|
||||
*
|
||||
* @param idChunk
|
||||
* @param expandedSpMap
|
||||
* @param theParams
|
||||
*/
|
||||
private void filterSearchByResourceIds(List<String> idChunk, SearchParameterMap expandedSpMap, ExportPIDIteratorParameters theParams) {
|
||||
ReferenceOrListParam orList = new ReferenceOrListParam();
|
||||
idChunk.forEach(id -> orList.add(new ReferenceParam(id)));
|
||||
expandedSpMap.add(getPatientSearchParamForCurrentResourceType(theParams.getResourceType()).getName(), orList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param idChunk
|
||||
* @param expandedSpMap
|
||||
*/
|
||||
private void filterSearchByHasParam(List<String> idChunk, SearchParameterMap expandedSpMap, ExportPIDIteratorParameters theParams) {
|
||||
HasOrListParam hasOrListParam = new HasOrListParam();
|
||||
idChunk.stream().forEach(id -> hasOrListParam.addOr(buildHasParam(id, theParams.getResourceType())));
|
||||
expandedSpMap.add("_has", hasOrListParam);
|
||||
}
|
||||
|
||||
private HasParam buildHasParam(String theId, String theResourceType) {
|
||||
if ("Practitioner".equalsIgnoreCase(theResourceType)) {
|
||||
return new HasParam("Patient", "general-practitioner", "_id", theId);
|
||||
} else if ("Organization".equalsIgnoreCase(theResourceType)) {
|
||||
return new HasParam("Patient", "organization", "_id", theId);
|
||||
} else {
|
||||
throw new IllegalArgumentException(Msg.code(2077) + " We can't handle forward references onto type " + theResourceType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the local myGroupId, perform an expansion to retrieve all resource IDs of member patients.
|
||||
* if myMdmEnabled is set to true, we also reach out to the IMdmLinkDao to attempt to also expand it into matched
|
||||
* patients.
|
||||
*
|
||||
* @return a Set of Strings representing the resource IDs of all members of a group.
|
||||
*/
|
||||
private Set<String> expandAllPatientPidsFromGroup(ExportPIDIteratorParameters theParams) {
|
||||
Set<String> expandedIds = new HashSet<>();
|
||||
SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions();
|
||||
IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(theParams.getGroupId()), requestDetails);
|
||||
Long pidOrNull = myJpaIdHelperService.getPidOrNull(group);
|
||||
|
||||
//Attempt to perform MDM Expansion of membership
|
||||
if (theParams.isExpandMdm()) {
|
||||
List<IMdmLinkDao.MdmPidTuple> goldenPidTargetPidTuples = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH);
|
||||
//Now lets translate these pids into resource IDs
|
||||
Set<Long> uniquePids = new HashSet<>();
|
||||
goldenPidTargetPidTuples.forEach(tuple -> {
|
||||
uniquePids.add(tuple.getGoldenPid());
|
||||
uniquePids.add(tuple.getSourcePid());
|
||||
});
|
||||
Map<Long, Optional<String>> pidToForcedIdMap = myIdHelperService.translatePidsToForcedIds(uniquePids);
|
||||
|
||||
Map<Long, Set<Long>> goldenResourceToSourcePidMap = new HashMap<>();
|
||||
extract(goldenPidTargetPidTuples, goldenResourceToSourcePidMap);
|
||||
populateMdmResourceCache(goldenPidTargetPidTuples);
|
||||
|
||||
//If the result of the translation is an empty optional, it means there is no forced id, and we can use the PID as the resource ID.
|
||||
Set<String> resolvedResourceIds = pidToForcedIdMap.entrySet().stream()
|
||||
.map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
expandedIds.addAll(resolvedResourceIds);
|
||||
}
|
||||
|
||||
//Now manually add the members of the group (its possible even with mdm expansion that some members dont have MDM matches,
|
||||
//so would be otherwise skipped
|
||||
expandedIds.addAll(getMembers(theParams.getGroupId()));
|
||||
|
||||
return expandedIds;
|
||||
}
|
||||
|
||||
/* Mdm Expansion */
|
||||
|
||||
private RuntimeSearchParam getRuntimeSearchParam(IBaseResource theResource) {
|
||||
Optional<RuntimeSearchParam> oPatientSearchParam = SearchParameterUtil.getOnlyPatientSearchParamForResourceType(myContext, theResource.fhirType());
|
||||
if (!oPatientSearchParam.isPresent()) {
|
||||
String errorMessage = String.format("[%s] has no search parameters that are for patients, so it is invalid for Group Bulk Export!", theResource.fhirType());
|
||||
throw new IllegalArgumentException(Msg.code(2103) + errorMessage);
|
||||
} else {
|
||||
return oPatientSearchParam.get();
|
||||
}
|
||||
}
|
||||
|
||||
private void annotateBackwardsReferences(IBaseResource iBaseResource) {
|
||||
Optional<String> patientReference = getPatientReference(iBaseResource);
|
||||
if (patientReference.isPresent()) {
|
||||
addGoldenResourceExtension(iBaseResource, patientReference.get());
|
||||
} else {
|
||||
ourLog.error("Failed to find the patient reference information for resource {}. This is a bug, " +
|
||||
"as all resources which can be exported via Group Bulk Export must reference a patient.", iBaseResource);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> getPatientReference(IBaseResource iBaseResource) {
|
||||
String fhirPath;
|
||||
|
||||
String resourceType = iBaseResource.fhirType();
|
||||
if (myResourceTypeToFhirPath.containsKey(resourceType)) {
|
||||
fhirPath = myResourceTypeToFhirPath.get(resourceType);
|
||||
} else {
|
||||
RuntimeSearchParam runtimeSearchParam = getRuntimeSearchParam(iBaseResource);
|
||||
fhirPath = getPatientFhirPath(runtimeSearchParam);
|
||||
myResourceTypeToFhirPath.put(resourceType, fhirPath);
|
||||
}
|
||||
|
||||
if (iBaseResource.fhirType().equalsIgnoreCase("Patient")) {
|
||||
return Optional.of(iBaseResource.getIdElement().getIdPart());
|
||||
} else {
|
||||
Optional<IBaseReference> optionalReference = getFhirParser().evaluateFirst(iBaseResource, fhirPath, IBaseReference.class);
|
||||
if (optionalReference.isPresent()) {
|
||||
return optionalReference.map(theIBaseReference -> theIBaseReference.getReferenceElement().getIdPart());
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addGoldenResourceExtension(IBaseResource iBaseResource, String sourceResourceId) {
|
||||
String goldenResourceId = myMdmExpansionCacheSvc.getGoldenResourceId(sourceResourceId);
|
||||
IBaseExtension<?, ?> extension = ExtensionUtil.getOrCreateExtension(iBaseResource, HapiExtensions.ASSOCIATED_GOLDEN_RESOURCE_EXTENSION_URL);
|
||||
if (!StringUtils.isBlank(goldenResourceId)) {
|
||||
ExtensionUtil.setExtension(myContext, extension, "reference", prefixPatient(goldenResourceId));
|
||||
}
|
||||
}
|
||||
|
||||
private String prefixPatient(String theResourceId) {
|
||||
return "Patient/" + theResourceId;
|
||||
}
|
||||
|
||||
private IFhirPath getFhirParser() {
|
||||
if (myFhirPath == null) {
|
||||
myFhirPath = myContext.newFhirPath();
|
||||
}
|
||||
return myFhirPath;
|
||||
}
|
||||
|
||||
private String getPatientFhirPath(RuntimeSearchParam theRuntimeParam) {
|
||||
String path = theRuntimeParam.getPath();
|
||||
// GGG: Yes this is a stupid hack, but by default this runtime search param will return stuff like
|
||||
// Observation.subject.where(resolve() is Patient) which unfortunately our FHIRpath evaluator doesn't play nicely with
|
||||
// our FHIRPath evaluator.
|
||||
if (path.contains(".where")) {
|
||||
path = path.substring(0, path.indexOf(".where"));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ package ca.uhn.fhir.jpa.bulk.imprt.job;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.bulk.export.job.CreateBulkExportEntityTasklet;
|
||||
import ca.uhn.fhir.util.ValidateUtil;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
|
@ -38,8 +37,16 @@ public class CreateBulkImportEntityTasklet implements Tasklet {
|
|||
|
||||
//We can leave early if they provided us with an existing job.
|
||||
ValidateUtil.isTrueOrThrowInvalidRequest(jobParameters.containsKey(BatchConstants.JOB_UUID_PARAMETER), "Job doesn't have a UUID");
|
||||
CreateBulkExportEntityTasklet.addUUIDToJobContext(theChunkContext, (String) jobParameters.get(BatchConstants.JOB_UUID_PARAMETER));
|
||||
addUUIDToJobContext(theChunkContext, (String) jobParameters.get(BatchConstants.JOB_UUID_PARAMETER));
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
|
||||
public void addUUIDToJobContext(ChunkContext theChunkContext, String theJobUUID) {
|
||||
theChunkContext
|
||||
.getStepContext()
|
||||
.getStepExecution()
|
||||
.getJobExecution()
|
||||
.getExecutionContext()
|
||||
.putString(BatchConstants.JOB_UUID_PARAMETER, theJobUUID);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkExportProcessor;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportHelperService;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.JpaBulkExportProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JpaBulkExportConfig {
|
||||
@Bean
|
||||
public IBulkExportProcessor jpaBulkExportProcessor() {
|
||||
return new JpaBulkExportProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BulkExportHelperService bulkExportHelperService() {
|
||||
return new BulkExportHelperService();
|
||||
}
|
||||
}
|
|
@ -16,10 +16,8 @@ import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
|||
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
||||
import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportJobSchedulingHelperImpl;
|
||||
import ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl;
|
||||
import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.imprt.svc.BulkDataImportSvcImpl;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
|
@ -181,7 +179,8 @@ import java.util.Date;
|
|||
BatchJobsConfig.class,
|
||||
SearchParamConfig.class,
|
||||
ValidationSupportConfig.class,
|
||||
Batch2SupportConfig.class
|
||||
Batch2SupportConfig.class,
|
||||
JpaBulkExportConfig.class
|
||||
})
|
||||
public class JpaConfig {
|
||||
public static final String JPA_VALIDATION_SUPPORT_CHAIN = "myJpaValidationSupportChain";
|
||||
|
@ -425,12 +424,6 @@ public class JpaConfig {
|
|||
return new AutowiringSpringBeanJobFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public IBulkDataExportSvc bulkDataExportSvc() {
|
||||
return new BulkDataExportSvcImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IBulkDataExportJobSchedulingHelper bulkDataExportJobSchedulingHelper() {
|
||||
return new BulkDataExportJobSchedulingHelperImpl();
|
||||
|
|
|
@ -20,9 +20,12 @@ package ca.uhn.fhir.jpa.dao.data;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
@ -31,6 +34,8 @@ import org.springframework.data.repository.query.Param;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface IBatch2JobInstanceRepository extends JpaRepository<Batch2JobInstanceEntity, String>, IHapiFhirJpaRepository {
|
||||
|
||||
@Modifying
|
||||
|
@ -45,6 +50,27 @@ public interface IBatch2JobInstanceRepository extends JpaRepository<Batch2JobIns
|
|||
@Query("UPDATE Batch2JobInstanceEntity e SET e.myCurrentGatedStepId = :currentGatedStepId WHERE e.myId = :id")
|
||||
void updateInstanceCurrentGatedStepId(@Param("id") String theInstanceId, @Param("currentGatedStepId") String theCurrentGatedStepId);
|
||||
|
||||
@Query(
|
||||
value = "SELECT * from Batch2JobInstanceEntity WHERE DEFINITION_ID = :defId AND PARAMS_JSON = :params AND STAT IN( :stats )",
|
||||
nativeQuery = true
|
||||
)
|
||||
List<JobInstance> findInstancesByJobIdParamsAndStatus(
|
||||
@Param("defId") String theDefinitionId,
|
||||
@Param("params") String theParams,
|
||||
@Param("stats") Set<StatusEnum> theStatus,
|
||||
Pageable thePageable
|
||||
);
|
||||
|
||||
@Query(
|
||||
value = "SELECT * from Batch2JobInstanceEntity WHERE DEFINITION_ID = :defId AND PARAMS_JSON = :params",
|
||||
nativeQuery = true
|
||||
)
|
||||
List<JobInstance> findInstancesByJobIdAndParams(
|
||||
@Param("defId") String theDefinitionId,
|
||||
@Param("params") String theParams,
|
||||
Pageable thePageable
|
||||
);
|
||||
|
||||
@Query("SELECT e FROM Batch2JobInstanceEntity e WHERE e.myDefinitionId = :jobDefinitionId AND e.myStatus IN :statuses")
|
||||
List<Batch2JobInstanceEntity> fetchInstancesByJobDefinitionIdAndStatus(@Param("jobDefinitionId") String theJobDefinitionId, @Param("statuses") Set<StatusEnum> theIncompleteStatuses, Pageable thePageRequest);
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ public interface IBatch2WorkChunkRepository extends JpaRepository<Batch2WorkChun
|
|||
@Query("SELECT e FROM Batch2WorkChunkEntity e WHERE e.myInstanceId = :instanceId ORDER BY e.mySequence ASC")
|
||||
List<Batch2WorkChunkEntity> fetchChunks(Pageable thePageRequest, @Param("instanceId") String theInstanceId);
|
||||
|
||||
@Query("SELECT e FROM Batch2WorkChunkEntity e WHERE e.myInstanceId = :instanceId AND e.myTargetStepId = :targetStepId ORDER BY e.mySequence ASC")
|
||||
List<Batch2WorkChunkEntity> fetchChunksForStep(Pageable thePageRequest, @Param("instanceId") String theInstanceId, @Param("targetStepId") String theTargetStepId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Batch2WorkChunkEntity e SET e.myStatus = :status, e.myEndTime = :et, e.myRecordsProcessed = :rp, e.mySerializedData = null WHERE e.myId = :id")
|
||||
void updateChunkStatusAndClearDataForEndSuccess(@Param("id") String theChunkId, @Param("et") Date theEndTime, @Param("rp") int theRecordsProcessed, @Param("status") StatusEnum theInProgress);
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.springframework.data.repository.query.Param;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
@Deprecated
|
||||
public interface IBulkExportCollectionDao extends JpaRepository<BulkExportCollectionEntity, Long>, IHapiFhirJpaRepository {
|
||||
|
||||
@Modifying
|
||||
|
|
|
@ -27,8 +27,16 @@ import java.io.Serializable;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
/*
|
||||
* These classes are no longer needed.
|
||||
* Metadata on the job is contained in the job itself
|
||||
* (no separate storage required).
|
||||
*
|
||||
* See the BulkExportAppCtx for job details
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "HFJ_BLK_EXPORT_COLLECTION")
|
||||
@Deprecated
|
||||
public class BulkExportCollectionEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
|
|
|
@ -35,8 +35,17 @@ import javax.persistence.SequenceGenerator;
|
|||
import javax.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*
|
||||
* These classes are no longer needed.
|
||||
* Metadata on the job is contained in the job itself
|
||||
* (no separate storage required).
|
||||
*
|
||||
* See the BulkExportAppCtx for job details
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "HFJ_BLK_EXPORT_COLFILE")
|
||||
@Deprecated
|
||||
public class BulkExportCollectionFileEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
|
|
|
@ -49,12 +49,21 @@ import java.util.Date;
|
|||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.left;
|
||||
|
||||
|
||||
/*
|
||||
* These classes are no longer needed.
|
||||
* Metadata on the job is contained in the job itself
|
||||
* (no separate storage required).
|
||||
*
|
||||
* See the BulkExportAppCtx for job details
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "HFJ_BLK_EXPORT_JOB", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "IDX_BLKEX_JOB_ID", columnNames = "JOB_ID")
|
||||
}, indexes = {
|
||||
@Index(name = "IDX_BLKEX_EXPTIME", columnList = "EXP_TIME")
|
||||
})
|
||||
@Deprecated
|
||||
public class BulkExportJobEntity implements Serializable {
|
||||
|
||||
public static final int REQUEST_LENGTH = 1024;
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class JobInstanceUtil {
|
||||
|
||||
private JobInstanceUtil() {}
|
||||
|
||||
/**
|
||||
* Converts a Batch2JobInstanceEntity into a JobInstance object
|
||||
* @param theEntity - the entity
|
||||
* @return - a job instance
|
||||
*/
|
||||
@Nonnull
|
||||
public static JobInstance fromEntityToInstance(@Nonnull Batch2JobInstanceEntity theEntity) {
|
||||
JobInstance retVal = new JobInstance();
|
||||
retVal.setInstanceId(theEntity.getId());
|
||||
retVal.setJobDefinitionId(theEntity.getDefinitionId());
|
||||
retVal.setJobDefinitionVersion(theEntity.getDefinitionVersion());
|
||||
retVal.setStatus(theEntity.getStatus());
|
||||
retVal.setCancelled(theEntity.isCancelled());
|
||||
retVal.setStartTime(theEntity.getStartTime());
|
||||
retVal.setCreateTime(theEntity.getCreateTime());
|
||||
retVal.setEndTime(theEntity.getEndTime());
|
||||
retVal.setCombinedRecordsProcessed(theEntity.getCombinedRecordsProcessed());
|
||||
retVal.setCombinedRecordsProcessedPerSecond(theEntity.getCombinedRecordsProcessedPerSecond());
|
||||
retVal.setTotalElapsedMillis(theEntity.getTotalElapsedMillis());
|
||||
retVal.setWorkChunksPurged(theEntity.getWorkChunksPurged());
|
||||
retVal.setProgress(theEntity.getProgress());
|
||||
retVal.setErrorMessage(theEntity.getErrorMessage());
|
||||
retVal.setErrorCount(theEntity.getErrorCount());
|
||||
retVal.setEstimatedTimeRemaining(theEntity.getEstimatedTimeRemaining());
|
||||
retVal.setParameters(theEntity.getParams());
|
||||
retVal.setCurrentGatedStepId(theEntity.getCurrentGatedStepId());
|
||||
retVal.setReport(theEntity.getReport());
|
||||
retVal.setEstimatedTimeRemaining(theEntity.getEstimatedTimeRemaining());
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Batch2WorkChunkEntity into a WorkChunk object
|
||||
* @param theEntity - the entity to convert
|
||||
* @param theIncludeData - whether or not to include the Data attached to the chunk
|
||||
* @return - the WorkChunk object
|
||||
*/
|
||||
@Nonnull
|
||||
public static WorkChunk fromEntityToWorkChunk(@Nonnull Batch2WorkChunkEntity theEntity, boolean theIncludeData) {
|
||||
WorkChunk retVal = new WorkChunk();
|
||||
retVal.setId(theEntity.getId());
|
||||
retVal.setSequence(theEntity.getSequence());
|
||||
retVal.setJobDefinitionId(theEntity.getJobDefinitionId());
|
||||
retVal.setJobDefinitionVersion(theEntity.getJobDefinitionVersion());
|
||||
retVal.setInstanceId(theEntity.getInstanceId());
|
||||
retVal.setTargetStepId(theEntity.getTargetStepId());
|
||||
retVal.setStatus(theEntity.getStatus());
|
||||
retVal.setCreateTime(theEntity.getCreateTime());
|
||||
retVal.setStartTime(theEntity.getStartTime());
|
||||
retVal.setEndTime(theEntity.getEndTime());
|
||||
retVal.setErrorMessage(theEntity.getErrorMessage());
|
||||
retVal.setErrorCount(theEntity.getErrorCount());
|
||||
retVal.setRecordsProcessed(theEntity.getRecordsProcessed());
|
||||
if (theIncludeData) {
|
||||
if (theEntity.getSerializedData() != null) {
|
||||
retVal.setData(theEntity.getSerializedData());
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
}
|
|
@ -1,20 +1,46 @@
|
|||
package ca.uhn.fhir.jpa.batch2;
|
||||
|
||||
import ca.uhn.fhir.batch2.api.JobOperationResultJson;
|
||||
import ca.uhn.fhir.batch2.model.FetchJobInstancesRequest;
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
||||
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
|
||||
import ca.uhn.fhir.jpa.util.JobInstanceUtil;
|
||||
import ca.uhn.fhir.model.api.PagingIterator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
@ -68,4 +94,244 @@ class JpaJobPersistenceImplTest {
|
|||
assertFalse(result.getSuccess());
|
||||
assertEquals("Job instance <test-instance-id> was already cancelled. Nothing to do.", result.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markInstanceAsCompleted_withInstanceId_updatesToCompleted() {
|
||||
// setup
|
||||
String jobId = "jobid";
|
||||
|
||||
// test
|
||||
mySvc.markInstanceAsCompleted(jobId);
|
||||
|
||||
// verify
|
||||
ArgumentCaptor<StatusEnum> statusCaptor = ArgumentCaptor.forClass(StatusEnum.class);
|
||||
verify(myJobInstanceRepository)
|
||||
.updateInstanceStatus(eq(jobId), statusCaptor.capture());
|
||||
assertEquals(StatusEnum.COMPLETED, statusCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteChunks_withInstanceId_callsChunkRepoDelete() {
|
||||
// setup
|
||||
String jobId = "jobid";
|
||||
|
||||
// test
|
||||
mySvc.deleteChunks(jobId);
|
||||
|
||||
// verify
|
||||
verify(myWorkChunkRepository)
|
||||
.deleteAllForInstance(eq(jobId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteInstanceAndChunks_withInstanceId_callsBothWorkchunkAndJobRespositoryDeletes() {
|
||||
// setup
|
||||
String jobid = "jobid";
|
||||
|
||||
// test
|
||||
mySvc.deleteInstanceAndChunks(jobid);
|
||||
|
||||
// verify
|
||||
verify(myWorkChunkRepository)
|
||||
.deleteAllForInstance(eq(jobid));
|
||||
verify(myJobInstanceRepository)
|
||||
.deleteById(eq(jobid));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateInstance_withInstance_checksInstanceExistsAndCallsSave() {
|
||||
// setup
|
||||
JobInstance toSave = createJobInstanceWithDemoData();
|
||||
Batch2JobInstanceEntity entity = new Batch2JobInstanceEntity();
|
||||
entity.setId(toSave.getInstanceId());
|
||||
|
||||
// when
|
||||
when(myJobInstanceRepository.findById(eq(toSave.getInstanceId())))
|
||||
.thenReturn(Optional.of(entity));
|
||||
|
||||
// test
|
||||
mySvc.updateInstance(toSave);
|
||||
|
||||
// verify
|
||||
ArgumentCaptor<Batch2JobInstanceEntity> entityCaptor = ArgumentCaptor.forClass(Batch2JobInstanceEntity.class);
|
||||
verify(myJobInstanceRepository)
|
||||
.save(entityCaptor.capture());
|
||||
Batch2JobInstanceEntity saved = entityCaptor.getValue();
|
||||
assertEquals(toSave.getInstanceId(), saved.getId());
|
||||
assertEquals(toSave.getStatus(), saved.getStatus());
|
||||
assertEquals(toSave.getStartTime(), entity.getStartTime());
|
||||
assertEquals(toSave.getEndTime(), entity.getEndTime());
|
||||
assertEquals(toSave.isCancelled(), entity.isCancelled());
|
||||
assertEquals(toSave.getCombinedRecordsProcessed(), entity.getCombinedRecordsProcessed());
|
||||
assertEquals(toSave.getCombinedRecordsProcessedPerSecond(), entity.getCombinedRecordsProcessedPerSecond());
|
||||
assertEquals(toSave.getTotalElapsedMillis(), entity.getTotalElapsedMillis());
|
||||
assertEquals(toSave.isWorkChunksPurged(), entity.getWorkChunksPurged());
|
||||
assertEquals(toSave.getProgress(), entity.getProgress());
|
||||
assertEquals(toSave.getErrorMessage(), entity.getErrorMessage());
|
||||
assertEquals(toSave.getErrorCount(), entity.getErrorCount());
|
||||
assertEquals(toSave.getEstimatedTimeRemaining(), entity.getEstimatedTimeRemaining());
|
||||
assertEquals(toSave.getCurrentGatedStepId(), entity.getCurrentGatedStepId());
|
||||
assertEquals(toSave.getReport(), entity.getReport());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateInstance_invalidId_throwsIllegalArgumentException() {
|
||||
// setup
|
||||
JobInstance instance = createJobInstanceWithDemoData();
|
||||
|
||||
// when
|
||||
when(myJobInstanceRepository.findById(anyString()))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
// test
|
||||
try {
|
||||
mySvc.updateInstance(instance);
|
||||
fail();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
assertTrue(ex.getMessage().contains("Unknown instance ID: " + instance.getInstanceId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchAllWorkChunksIterator_withValidIdAndBoolToSayToIncludeData_returnsPagingIterator() {
|
||||
// setup
|
||||
String instanceId = "instanceId";
|
||||
String jobDefinition = "definitionId";
|
||||
int version = 1;
|
||||
String targetStep = "step";
|
||||
|
||||
List<Batch2WorkChunkEntity> workChunkEntityList = new ArrayList<>();
|
||||
Batch2WorkChunkEntity chunk1 = new Batch2WorkChunkEntity();
|
||||
chunk1.setId("id1");
|
||||
chunk1.setJobDefinitionVersion(version);
|
||||
chunk1.setJobDefinitionId(jobDefinition);
|
||||
chunk1.setSerializedData("serialized data 1");
|
||||
chunk1.setTargetStepId(targetStep);
|
||||
workChunkEntityList.add(chunk1);
|
||||
Batch2WorkChunkEntity chunk2 = new Batch2WorkChunkEntity();
|
||||
chunk2.setId("id2");
|
||||
chunk2.setSerializedData("serialized data 2");
|
||||
chunk2.setJobDefinitionId(jobDefinition);
|
||||
chunk2.setJobDefinitionVersion(version);
|
||||
chunk2.setTargetStepId(targetStep);
|
||||
workChunkEntityList.add(chunk2);
|
||||
|
||||
for (boolean includeData : new boolean[] { true , false }) {
|
||||
// when
|
||||
when(myWorkChunkRepository.fetchChunks(any(PageRequest.class), eq(instanceId)))
|
||||
.thenReturn(workChunkEntityList);
|
||||
|
||||
// test
|
||||
Iterator<WorkChunk> chunkIterator = mySvc.fetchAllWorkChunksIterator(instanceId, includeData);
|
||||
|
||||
// verify
|
||||
assertTrue(chunkIterator instanceof PagingIterator);
|
||||
verify(myWorkChunkRepository, never())
|
||||
.fetchChunks(any(PageRequest.class), anyString());
|
||||
|
||||
// now try the iterator out...
|
||||
WorkChunk chunk = chunkIterator.next();
|
||||
assertEquals(chunk1.getId(), chunk.getId());
|
||||
if (includeData) {
|
||||
assertEquals(chunk1.getSerializedData(), chunk.getData());
|
||||
} else {
|
||||
assertNull(chunk.getData());
|
||||
}
|
||||
chunk = chunkIterator.next();
|
||||
assertEquals(chunk2.getId(), chunk.getId());
|
||||
if (includeData) {
|
||||
assertEquals(chunk2.getSerializedData(), chunk.getData());
|
||||
} else {
|
||||
assertNull(chunk.getData());
|
||||
}
|
||||
|
||||
verify(myWorkChunkRepository)
|
||||
.fetchChunks(any(PageRequest.class), eq(instanceId));
|
||||
|
||||
reset(myWorkChunkRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchInstances_validRequest_returnsFoundInstances() {
|
||||
// setup
|
||||
int pageStart = 1;
|
||||
int pageSize = 132;
|
||||
JobInstance job1 = createJobInstanceWithDemoData();
|
||||
FetchJobInstancesRequest req = new FetchJobInstancesRequest(job1.getInstanceId(), "params");
|
||||
JobInstance job2 = createJobInstanceWithDemoData();
|
||||
List<JobInstance> instances = Arrays.asList(job1, job2);
|
||||
|
||||
// when
|
||||
when(myJobInstanceRepository
|
||||
.findInstancesByJobIdAndParams(eq(req.getJobDefinition()),
|
||||
eq(req.getParameters()),
|
||||
any(Pageable.class)))
|
||||
.thenReturn(instances);
|
||||
|
||||
// test
|
||||
List<JobInstance> retInstances = mySvc.fetchInstances(req, pageStart, pageSize);
|
||||
|
||||
// verify
|
||||
assertEquals(instances.size(), retInstances.size());
|
||||
assertEquals(instances, retInstances);
|
||||
|
||||
ArgumentCaptor<Pageable> pageableCaptor = ArgumentCaptor.forClass(Pageable.class);
|
||||
verify(myJobInstanceRepository)
|
||||
.findInstancesByJobIdAndParams(
|
||||
eq(req.getJobDefinition()),
|
||||
eq(req.getParameters()),
|
||||
pageableCaptor.capture()
|
||||
);
|
||||
|
||||
Pageable pageable = pageableCaptor.getValue();
|
||||
assertEquals(pageStart, pageable.getPageNumber());
|
||||
assertEquals(pageSize, pageable.getPageSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchInstance_validId_returnsInstance() {
|
||||
// setup
|
||||
Batch2JobInstanceEntity entity = createBatch2JobInstanceEntity();
|
||||
JobInstance instance = createJobInstanceFromEntity(entity);
|
||||
|
||||
// when
|
||||
when(myJobInstanceRepository.findById(eq(instance.getInstanceId())))
|
||||
.thenReturn(Optional.of(entity));
|
||||
|
||||
// test
|
||||
Optional<JobInstance> retInstance = mySvc.fetchInstance(entity.getId());
|
||||
|
||||
// verify
|
||||
assertTrue(retInstance.isPresent());
|
||||
assertEquals(instance.getInstanceId(), retInstance.get().getInstanceId());
|
||||
}
|
||||
|
||||
private JobInstance createJobInstanceWithDemoData() {
|
||||
return createJobInstanceFromEntity(createBatch2JobInstanceEntity());
|
||||
}
|
||||
|
||||
private JobInstance createJobInstanceFromEntity(Batch2JobInstanceEntity theEntity) {
|
||||
return JobInstanceUtil.fromEntityToInstance(theEntity);
|
||||
}
|
||||
|
||||
private Batch2JobInstanceEntity createBatch2JobInstanceEntity() {
|
||||
Batch2JobInstanceEntity entity = new Batch2JobInstanceEntity();
|
||||
entity.setId("id");
|
||||
entity.setStartTime(new Date(2000, 1, 2));
|
||||
entity.setEndTime(new Date(2000, 2, 3));
|
||||
entity.setStatus(StatusEnum.COMPLETED);
|
||||
entity.setCancelled(true);
|
||||
entity.setCombinedRecordsProcessed(12);
|
||||
entity.setCombinedRecordsProcessedPerSecond(2d);
|
||||
entity.setTotalElapsedMillis(1000);
|
||||
entity.setWorkChunksPurged(true);
|
||||
entity.setProgress(22d);
|
||||
entity.setErrorMessage("1232");
|
||||
entity.setErrorCount(9);
|
||||
entity.setEstimatedTimeRemaining("blah");
|
||||
entity.setCurrentGatedStepId("stepid");
|
||||
entity.setReport("report");
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,440 @@
|
|||
package ca.uhn.fhir.jpa.bulk.export.svc;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.fhirpath.IFhirPath;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.ExportPIDIteratorParameters;
|
||||
import ca.uhn.fhir.jpa.dao.IResultIterator;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
|
||||
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IJpaIdHelperService;
|
||||
import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Group;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
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.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
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.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class JpaBulkExportProcessorTest {
|
||||
|
||||
private class ListResultIterator implements IResultIterator {
|
||||
|
||||
private List<ResourcePersistentId> myList;
|
||||
|
||||
private int index;
|
||||
|
||||
public ListResultIterator(List<ResourcePersistentId> theList) {
|
||||
myList = theList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSkippedCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNonSkippedCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return index < myList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePersistentId next() {
|
||||
return myList.get(index++);
|
||||
}
|
||||
}
|
||||
|
||||
@Spy
|
||||
private FhirContext myFhirContext = FhirContext.forR4Cached();
|
||||
|
||||
@Mock
|
||||
private BulkExportHelperService myBulkExportHelperService;
|
||||
|
||||
@Mock
|
||||
private DaoConfig myDaoConfig;
|
||||
|
||||
@Mock
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Mock
|
||||
private SearchBuilderFactory mySearchBuilderFactory;
|
||||
|
||||
@Mock
|
||||
private IIdHelperService myIdHelperService;
|
||||
|
||||
@Mock
|
||||
private IMdmLinkDao myMdmLinkDao;
|
||||
|
||||
@Mock
|
||||
private IJpaIdHelperService myJpaIdHelperService;
|
||||
|
||||
@Mock
|
||||
private MdmExpansionCacheSvc myMdmExpansionCacheSvc;
|
||||
|
||||
@InjectMocks
|
||||
private JpaBulkExportProcessor myProcessor;
|
||||
|
||||
private ExportPIDIteratorParameters createExportParameters(BulkDataExportOptions.ExportStyle theExportStyle) {
|
||||
ExportPIDIteratorParameters parameters = new ExportPIDIteratorParameters();
|
||||
parameters.setJobId("jobId");
|
||||
parameters.setExportStyle(theExportStyle);
|
||||
if (theExportStyle == BulkDataExportOptions.ExportStyle.GROUP) {
|
||||
parameters.setGroupId("123");
|
||||
}
|
||||
parameters.setStartDate(new Date());
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private List<IPrimitiveType> createPatientTypes() {
|
||||
long id1 = 123;
|
||||
long id2 = 456;
|
||||
String patient1Id = "Patient/" + id1;
|
||||
String patient2Id = "Patient/" + id2;
|
||||
|
||||
List<IPrimitiveType> patientTypes = Arrays.asList(
|
||||
new IdDt(patient1Id),
|
||||
new IdDt(patient2Id)
|
||||
);
|
||||
return patientTypes;
|
||||
}
|
||||
|
||||
private IMdmLinkDao.MdmPidTuple createTuple(long theGroupId, long theGoldenId) {
|
||||
return new IMdmLinkDao.MdmPidTuple() {
|
||||
@Override
|
||||
public Long getGoldenPid() {
|
||||
return theGoldenId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getSourcePid() {
|
||||
return theGroupId;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourcePidIterator_paramsWithPatientExportStyle_returnsAnIterator() {
|
||||
// setup
|
||||
ExportPIDIteratorParameters parameters = createExportParameters(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
parameters.setResourceType("Patient");
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
List<SearchParameterMap> maps = new ArrayList<>();
|
||||
maps.add(map);
|
||||
|
||||
ResourcePersistentId pid = new ResourcePersistentId("Patient/123");
|
||||
ResourcePersistentId pid2 = new ResourcePersistentId("Observation/123");
|
||||
ListResultIterator resultIterator = new ListResultIterator(
|
||||
Arrays.asList(pid, pid2)
|
||||
);
|
||||
|
||||
// extra mocks
|
||||
IFhirResourceDao<?> mockDao = mock(IFhirResourceDao.class);
|
||||
ISearchBuilder searchBuilder = mock(ISearchBuilder.class);
|
||||
|
||||
// when
|
||||
when(myDaoConfig.getIndexMissingFields())
|
||||
.thenReturn(DaoConfig.IndexEnabledEnum.ENABLED);
|
||||
when(myBulkExportHelperService.createSearchParameterMapsForResourceType(any(RuntimeResourceDefinition.class), eq(parameters)))
|
||||
.thenReturn(maps);
|
||||
// from getSearchBuilderForLocalResourceType
|
||||
when(myDaoRegistry.getResourceDao(anyString()))
|
||||
.thenReturn(mockDao);
|
||||
when(mySearchBuilderFactory.newSearchBuilder(eq(mockDao), eq(parameters.getResourceType()), any()))
|
||||
.thenReturn(searchBuilder);
|
||||
// ret
|
||||
when(searchBuilder.createQuery(
|
||||
eq(map),
|
||||
any(SearchRuntimeDetails.class),
|
||||
any(),
|
||||
eq(RequestPartitionId.allPartitions())))
|
||||
.thenReturn(resultIterator);
|
||||
|
||||
// test
|
||||
Iterator<ResourcePersistentId> pidIterator = myProcessor.getResourcePidIterator(parameters);
|
||||
|
||||
// verify
|
||||
assertNotNull(pidIterator);
|
||||
assertTrue(pidIterator.hasNext());
|
||||
assertEquals(pid, pidIterator.next());
|
||||
assertTrue(pidIterator.hasNext());
|
||||
assertEquals(pid2, pidIterator.next());
|
||||
assertFalse(pidIterator.hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourcePidIterator_patientStyleWithIndexMissingFieldsDisabled_throws() {
|
||||
// setup
|
||||
ExportPIDIteratorParameters parameters = createExportParameters(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
parameters.setResourceType("Patient");
|
||||
|
||||
// when
|
||||
when(myDaoConfig.getIndexMissingFields())
|
||||
.thenReturn(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
// test
|
||||
try {
|
||||
myProcessor.getResourcePidIterator(parameters);
|
||||
fail();
|
||||
} catch (IllegalStateException ex) {
|
||||
assertTrue(ex.getMessage().contains("You attempted to start a Patient Bulk Export,"));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
booleans = { true, false }
|
||||
)
|
||||
public void getResourcePidIterator_groupExportStyleWithPatientResource_returnsIterator(boolean theMdm) {
|
||||
// setup
|
||||
ExportPIDIteratorParameters parameters = createExportParameters(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
parameters.setResourceType("Patient");
|
||||
|
||||
long groupId = Long.parseLong(parameters.getGroupId());
|
||||
long groupGoldenPid = 4567l;
|
||||
|
||||
Group groupResource = new Group();
|
||||
groupResource.setId(parameters.getGroupId());
|
||||
|
||||
List<IPrimitiveType> patientTypes = createPatientTypes();
|
||||
List<Long> pids = new ArrayList<>();
|
||||
for (IPrimitiveType type : patientTypes) {
|
||||
pids.add(((IdDt) type).getIdPartAsLong());
|
||||
}
|
||||
|
||||
IMdmLinkDao.MdmPidTuple tuple = createTuple(groupId, groupGoldenPid);
|
||||
|
||||
IFhirResourceDao<Group> groupDao = mock(IFhirResourceDao.class);
|
||||
IFhirPath path = mock(IFhirPath.class);
|
||||
parameters.setExpandMdm(theMdm); // set mdm expansion
|
||||
|
||||
// when
|
||||
// getMembers
|
||||
when(myDaoRegistry.getResourceDao(eq("Group")))
|
||||
.thenReturn(groupDao);
|
||||
when(groupDao.read(eq(new IdDt(parameters.getGroupId())), any(SystemRequestDetails.class)))
|
||||
.thenReturn(groupResource);
|
||||
when(myFhirContext.newFhirPath())
|
||||
.thenReturn(path);
|
||||
when(path.evaluate(eq(groupResource), anyString(), any(Class.class)))
|
||||
.thenReturn(patientTypes);
|
||||
when(myJpaIdHelperService.getPidsOrThrowException(anyList()))
|
||||
.thenReturn(pids);
|
||||
// mdm expansion stuff
|
||||
if (theMdm) {
|
||||
when(myJpaIdHelperService.getPidOrNull(any(Group.class)))
|
||||
.thenReturn(groupId);
|
||||
when(myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(anyLong(), eq(MdmMatchResultEnum.MATCH)))
|
||||
.thenReturn(Collections.singletonList(tuple));
|
||||
when(myMdmExpansionCacheSvc.hasBeenPopulated())
|
||||
.thenReturn(false); // does not matter, since if false, it then goes and populates
|
||||
}
|
||||
|
||||
// test
|
||||
Iterator<ResourcePersistentId> pidIterator = myProcessor.getResourcePidIterator(parameters);
|
||||
|
||||
// verify
|
||||
assertNotNull(pidIterator);
|
||||
int count = 0;
|
||||
assertTrue(pidIterator.hasNext());
|
||||
while (pidIterator.hasNext()) {
|
||||
ResourcePersistentId pid = pidIterator.next();
|
||||
long idAsLong = pid.getIdAsLong();
|
||||
boolean existing = pids.contains(idAsLong);
|
||||
if (!existing) {
|
||||
assertTrue(theMdm);
|
||||
assertEquals(groupGoldenPid, idAsLong);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int total = pids.size();
|
||||
assertEquals(total, count);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = { false, true })
|
||||
public void getResourcePidIterator_groupExportNonPatient_returnsIterator(boolean theMdm) {
|
||||
// setup
|
||||
ExportPIDIteratorParameters parameters = createExportParameters(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
parameters.setResourceType("Observation");
|
||||
|
||||
long groupId = Long.parseLong(parameters.getGroupId());
|
||||
Group groupResource = new Group();
|
||||
groupResource.setId(parameters.getGroupId());
|
||||
long groupGoldenPid = 4567l;
|
||||
|
||||
ResourcePersistentId pid = new ResourcePersistentId("Patient/123");
|
||||
ResourcePersistentId pid2 = new ResourcePersistentId("Observation/123");
|
||||
ListResultIterator resultIterator = new ListResultIterator(
|
||||
Arrays.asList(pid, pid2)
|
||||
);
|
||||
|
||||
IMdmLinkDao.MdmPidTuple tuple = createTuple(groupId, groupGoldenPid);
|
||||
List<IPrimitiveType> patientTypes = createPatientTypes();
|
||||
|
||||
IFhirResourceDao<Group> groupDao = mock(IFhirResourceDao.class);
|
||||
IFhirResourceDao<Observation> observationDao = mock(IFhirResourceDao.class);
|
||||
parameters.setExpandMdm(theMdm); // set mdm expansion
|
||||
IFhirPath path = mock(IFhirPath.class);
|
||||
ISearchBuilder searchBuilder = mock(ISearchBuilder.class);
|
||||
|
||||
// when
|
||||
// getMembers
|
||||
when(myDaoRegistry.getResourceDao(eq("Group")))
|
||||
.thenReturn(groupDao);
|
||||
when(groupDao.read(any(IIdType.class), any(SystemRequestDetails.class)))
|
||||
.thenReturn(groupResource);
|
||||
when(myJpaIdHelperService.getPidOrNull(eq(groupResource)))
|
||||
.thenReturn(groupId);
|
||||
|
||||
if (theMdm) {
|
||||
when(myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(anyLong(), eq(MdmMatchResultEnum.MATCH)))
|
||||
.thenReturn(Collections.singletonList(tuple));
|
||||
when(myIdHelperService.translatePidsToForcedIds(any(Set.class)))
|
||||
.thenAnswer(params -> {
|
||||
Set<Long> uniqPids = params.getArgument(0);
|
||||
HashMap<Long, Optional<String>> answer = new HashMap<>();
|
||||
for (long l : uniqPids) {
|
||||
answer.put(l, Optional.empty());
|
||||
}
|
||||
return answer;
|
||||
});
|
||||
}
|
||||
when(myFhirContext.newFhirPath())
|
||||
.thenReturn(path);
|
||||
when(path.evaluate(eq(groupResource), anyString(), any(Class.class)))
|
||||
.thenReturn(patientTypes);
|
||||
when(myBulkExportHelperService.createSearchParameterMapsForResourceType(any(RuntimeResourceDefinition.class), any(ExportPIDIteratorParameters.class)))
|
||||
.thenReturn(Collections.singletonList(new SearchParameterMap()));
|
||||
when(myDaoRegistry.getResourceDao(eq("Observation")))
|
||||
.thenReturn(observationDao);
|
||||
when(mySearchBuilderFactory.newSearchBuilder(any(), eq("Observation"), any()))
|
||||
.thenReturn(searchBuilder);
|
||||
when(searchBuilder.createQuery(any(SearchParameterMap.class),
|
||||
any(SearchRuntimeDetails.class),
|
||||
any(),
|
||||
eq(RequestPartitionId.allPartitions())))
|
||||
.thenReturn(resultIterator);
|
||||
|
||||
// test
|
||||
Iterator<ResourcePersistentId> pidIterator = myProcessor.getResourcePidIterator(parameters);
|
||||
|
||||
// verify
|
||||
assertNotNull(pidIterator, "PID iterator null for mdm = " + theMdm);
|
||||
assertTrue(pidIterator.hasNext(), "PID iterator empty for mdm = " + theMdm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResourcePidIterator_systemExport_returnsIterator() {
|
||||
// setup
|
||||
ExportPIDIteratorParameters parameters = createExportParameters(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
parameters.setResourceType("Patient");
|
||||
|
||||
ResourcePersistentId pid = new ResourcePersistentId("Patient/123");
|
||||
ResourcePersistentId pid2 = new ResourcePersistentId("Observation/123");
|
||||
ListResultIterator resultIterator = new ListResultIterator(
|
||||
Arrays.asList(pid, pid2)
|
||||
);
|
||||
|
||||
// extra mocks
|
||||
IFhirResourceDao<Patient> dao = mock(IFhirResourceDao.class);
|
||||
ISearchBuilder searchBuilder = mock(ISearchBuilder.class);
|
||||
|
||||
// when
|
||||
when(myBulkExportHelperService.createSearchParameterMapsForResourceType(
|
||||
any(RuntimeResourceDefinition.class),
|
||||
any(ExportPIDIteratorParameters.class)
|
||||
)).thenReturn(Collections.singletonList(new SearchParameterMap()));
|
||||
when(myDaoRegistry.getResourceDao(eq("Patient")))
|
||||
.thenReturn(dao);
|
||||
when(mySearchBuilderFactory.newSearchBuilder(
|
||||
any(IFhirResourceDao.class),
|
||||
anyString(),
|
||||
any()
|
||||
)).thenReturn(searchBuilder);
|
||||
when(searchBuilder.createQuery(
|
||||
any(SearchParameterMap.class),
|
||||
any(SearchRuntimeDetails.class),
|
||||
any(),
|
||||
eq(RequestPartitionId.allPartitions())
|
||||
)).thenReturn(resultIterator);
|
||||
|
||||
// test
|
||||
Iterator<ResourcePersistentId> iterator = myProcessor.getResourcePidIterator(parameters);
|
||||
|
||||
// verify
|
||||
assertNotNull(iterator);
|
||||
assertTrue(iterator.hasNext());
|
||||
int count = 0;
|
||||
while (iterator.hasNext()) {
|
||||
ResourcePersistentId ret = iterator.next();
|
||||
assertTrue(
|
||||
ret.equals(pid) || ret.equals(pid2)
|
||||
);
|
||||
count++;
|
||||
}
|
||||
assertEquals(2, count);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
|
||||
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
|
||||
|
@ -187,7 +188,8 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
|
|||
JobInstanceStartRequest request = new JobInstanceStartRequest();
|
||||
request.setJobDefinitionId(MdmClearAppCtx.JOB_MDM_CLEAR);
|
||||
request.setParameters(params);
|
||||
String id = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
|
||||
String id = response.getJobId();
|
||||
|
||||
IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext);
|
||||
ParametersUtil.addParameterToParametersString(myFhirContext, retVal, ProviderConstants.OPERATION_BATCH_RESPONSE_JOB_ID, id);
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.Patient;
|
|||
import org.hl7.fhir.r4.model.Practitioner;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -121,7 +122,22 @@ public class MdmProviderClearLinkR4Test extends BaseLinkR4Test {
|
|||
return new SearchParameterMap().setLoadSynchronous(true).add("_tag", new TokenParam(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("Intermittent")
|
||||
// TODO KHS I know intermittent tags aren't used by hapi but this will help me find this test when I review intermittents.
|
||||
// Last time this test failed, this is what was in the logs:
|
||||
// 2022-07-17 19:57:27.103 [main] INFO c.u.f.batch2.channel.BatchJobSender [BatchJobSender.java:43] Sending work notification for job[MDM_CLEAR] instance[6f6d6fc5-f74a-426f-b215-7a383893f4bc] step[generate-ranges] chunk[219e29d5-1ee7-47dd-99a1-c636b1b221ae]
|
||||
// 2022-07-17 19:57:27.193 [batch2-work-notification-1] INFO c.u.f.m.b.MdmGenerateRangeChunksStep [MdmGenerateRangeChunksStep.java:49] Initiating mdm clear of [Patient]] Golden Resources from Sat Jan 01 00:00:00 UTC 2000 to Sun Jul 17 19:57:27 UTC 2022
|
||||
// 2022-07-17 19:57:27.275 [batch2-work-notification-1] INFO c.u.f.m.b.MdmGenerateRangeChunksStep [MdmGenerateRangeChunksStep.java:49] Initiating mdm clear of [Practitioner]] Golden Resources from Sat Jan 01 00:00:00 UTC 2000 to Sun Jul 17 19:57:27 UTC 2022
|
||||
// 2022-07-17 19:57:27.381 [awaitility-thread] INFO c.u.f.b.p.JobInstanceProgressCalculator [JobInstanceProgressCalculator.java:67] Job 6f6d6fc5-f74a-426f-b215-7a383893f4bc of type MDM_CLEAR has status IN_PROGRESS - 0 records processed (null/sec) - ETA: null
|
||||
// 2022-07-17 19:57:27.510 [awaitility-thread] INFO c.u.f.b.p.JobInstanceProgressCalculator [JobInstanceProgressCalculator.java:67] Job 6f6d6fc5-f74a-426f-b215-7a383893f4bc of type MDM_CLEAR has status IN_PROGRESS - 0 records processed (null/sec) - ETA: null
|
||||
// 2022-07-17 19:57:37.175 [awaitility-thread] INFO c.u.f.b.p.JobInstanceProgressCalculator [JobInstanceProgressCalculator.java:67] Job 6f6d6fc5-f74a-426f-b215-7a383893f4bc of type MDM_CLEAR has status IN_PROGRESS - 0 records processed (null/sec) - ETA: null
|
||||
// 2022-07-17 19:57:37.329 [main] INFO c.u.f.m.r.config.MdmRuleValidator [MdmRuleValidator.java:116] Validating MDM types [Patient, Practitioner, Medication]
|
||||
// 2022-07-17 19:57:37.330 [main] INFO c.u.f.m.r.config.MdmRuleValidator [MdmRuleValidator.java:133] Validating search parameters [ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson@799225ca, ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson@f03b50d, ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson@5f19ad6b, ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson@4976b9, ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson@681dbf0f]
|
||||
// 2022-07-17 19:57:37.330 [main] INFO c.u.f.m.r.config.MdmRuleValidator [MdmRuleValidator.java:161] Validating match fields [ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@7aa4d4dc, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@68444c16, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@23f30319, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@261325af, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@7acd1785, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@30a3d036, ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson@bf3e6f0]
|
||||
// 2022-07-17 19:57:37.330 [main] INFO c.u.f.m.r.config.MdmRuleValidator [MdmRuleValidator.java:253] Validating system URI http://company.io/fhir/NamingSystem/custom-eid-system
|
||||
// 2022-07-17 19:57:37.335 [main] INFO c.u.f.j.s.r.ResourceReindexingSvcImpl [ResourceReindexingSvcImpl.java:235] Cancelling and purging all resource reindexing jobs
|
||||
|
||||
// @Test
|
||||
public void testGoldenResourceWithCircularReferenceCanBeCleared() {
|
||||
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient());
|
||||
Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient());
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator;
|
|||
import ca.uhn.fhir.batch2.api.IJobMaintenanceService;
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import org.awaitility.core.ConditionTimeoutException;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -47,6 +48,10 @@ public class Batch2JobHelper {
|
|||
@Autowired
|
||||
private IJobCoordinator myJobCoordinator;
|
||||
|
||||
public JobInstance awaitJobCompletion(Batch2JobStartResponse theStartResponse) {
|
||||
return awaitJobCompletion(theStartResponse.getJobId());
|
||||
}
|
||||
|
||||
public JobInstance awaitJobCompletion(String theId) {
|
||||
await().until(() -> {
|
||||
myJobMaintenanceService.runMaintenancePass();
|
||||
|
@ -55,10 +60,18 @@ public class Batch2JobHelper {
|
|||
return myJobCoordinator.getInstance(theId);
|
||||
}
|
||||
|
||||
public void awaitSingleChunkJobCompletion(Batch2JobStartResponse theStartResponse) {
|
||||
awaitSingleChunkJobCompletion(theStartResponse.getJobId());
|
||||
}
|
||||
|
||||
public void awaitSingleChunkJobCompletion(String theId) {
|
||||
await().until(() -> myJobCoordinator.getInstance(theId).getStatus() == StatusEnum.COMPLETED);
|
||||
}
|
||||
|
||||
public JobInstance awaitJobFailure(Batch2JobStartResponse theStartResponse) {
|
||||
return awaitJobFailure(theStartResponse.getJobId());
|
||||
}
|
||||
|
||||
public JobInstance awaitJobFailure(String theId) {
|
||||
await().until(() -> {
|
||||
myJobMaintenanceService.runMaintenancePass();
|
||||
|
|
|
@ -23,6 +23,7 @@ import ca.uhn.fhir.batch2.model.StatusEnum;
|
|||
import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.jpa.test.Batch2JobHelper;
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
|
@ -111,10 +112,10 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest request = buildRequest(jobId);
|
||||
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
myFirstStepLatch.awaitExpected();
|
||||
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(instanceId);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(startResponse.getJobId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -135,13 +136,13 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
myLastStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
myFirstStepLatch.awaitExpected();
|
||||
|
||||
myBatch2JobHelper.assertNoGatedStep(instanceId);
|
||||
myBatch2JobHelper.assertNoGatedStep(startResponse.getJobId());
|
||||
|
||||
// Since there was only one chunk, the job should proceed without requiring a maintenance pass
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(instanceId);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(startResponse.getJobId());
|
||||
myLastStepLatch.awaitExpected();
|
||||
}
|
||||
|
||||
|
@ -178,7 +179,8 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
myFirstStepLatch.setExpectedCount(1);
|
||||
myLastStepLatch.setExpectedCount(1);
|
||||
calledLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
myFirstStepLatch.awaitExpected();
|
||||
calledLatch.awaitExpected();
|
||||
|
||||
|
@ -283,9 +285,11 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
// run test
|
||||
JobInstanceStartRequest request = buildRequest(jobId);
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
myFirstStepLatch.awaitExpected();
|
||||
|
||||
|
||||
myBatch2JobHelper.awaitGatedStepId(FIRST_STEP_ID, instanceId);
|
||||
|
||||
// wait for last step to finish
|
||||
|
@ -327,7 +331,8 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest request = buildRequest(jobId);
|
||||
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
myFirstStepLatch.awaitExpected();
|
||||
|
||||
myBatch2JobHelper.awaitGatedStepId(FIRST_STEP_ID, instanceId);
|
||||
|
@ -354,7 +359,8 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest request = buildRequest(jobId);
|
||||
|
||||
// execute
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
|
||||
// validate
|
||||
myBatch2JobHelper.awaitJobFailure(instanceId);
|
||||
|
@ -378,7 +384,8 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
|
||||
// execute
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
myFirstStepLatch.awaitExpected();
|
||||
|
||||
// validate
|
||||
|
@ -441,8 +448,8 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
|
|||
// test
|
||||
JobInstanceStartRequest request = buildRequest(jobId);
|
||||
myFirstStepLatch.setExpectedCount(1);
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
JobInstance instance = myBatch2JobHelper.awaitJobHitsStatusInTime(instanceId,
|
||||
Batch2JobStartResponse response = myJobCoordinator.startInstance(request);
|
||||
JobInstance instance = myBatch2JobHelper.awaitJobHitsStatusInTime(response.getJobId(),
|
||||
12, // we want to wait a long time (2 min here) cause backoff is incremental
|
||||
StatusEnum.FAILED, StatusEnum.ERRORED // error states
|
||||
);
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package ca.uhn.fhir.jpa.bulk;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.api.model.Batch2JobInfo;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
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.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson;
|
||||
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
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.client.apache.ResourceEntity;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
|
@ -17,7 +19,6 @@ import ca.uhn.fhir.test.utilities.JettyUtil;
|
|||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
@ -37,13 +38,18 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -54,11 +60,11 @@ import static org.hamcrest.Matchers.is;
|
|||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -70,17 +76,17 @@ public class BulkDataExportProviderTest {
|
|||
private static final String GROUP_ID = "Group/G2401";
|
||||
private static final String G_JOB_ID = "0000000-GGGGGG";
|
||||
private Server myServer;
|
||||
@Spy
|
||||
private final FhirContext myCtx = FhirContext.forR4Cached();
|
||||
private int myPort;
|
||||
|
||||
@Mock
|
||||
private IBulkDataExportSvc myBulkDataExportSvc;
|
||||
@Mock
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
private IBatch2JobRunner myJobRunner;
|
||||
|
||||
private CloseableHttpClient myClient;
|
||||
@Captor
|
||||
private ArgumentCaptor<BulkDataExportOptions> myBulkDataExportOptionsCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<Boolean> myBooleanArgumentCaptor;
|
||||
|
||||
@InjectMocks
|
||||
private BulkDataExportProvider myProvider;
|
||||
|
||||
@AfterEach
|
||||
public void after() throws Exception {
|
||||
|
@ -92,13 +98,9 @@ public class BulkDataExportProviderTest {
|
|||
public void start() throws Exception {
|
||||
myServer = new Server(0);
|
||||
|
||||
BulkDataExportProvider provider = new BulkDataExportProvider();
|
||||
provider.setBulkDataExportSvcForUnitTests(myBulkDataExportSvc);
|
||||
provider.setFhirContextForUnitTest(myCtx);
|
||||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
RestfulServer servlet = new RestfulServer(myCtx);
|
||||
servlet.registerProvider(provider);
|
||||
servlet.registerProvider(myProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||
myServer.setHandler(proxyHandler);
|
||||
|
@ -109,26 +111,46 @@ public class BulkDataExportProviderTest {
|
|||
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||
builder.setConnectionManager(connectionManager);
|
||||
myClient = builder.build();
|
||||
}
|
||||
|
||||
private BulkExportParameters verifyJobStart() {
|
||||
ArgumentCaptor<Batch2BaseJobParameters> startJobCaptor = ArgumentCaptor.forClass(Batch2BaseJobParameters.class);
|
||||
verify(myJobRunner).startNewJob(startJobCaptor.capture());
|
||||
Batch2BaseJobParameters sp = startJobCaptor.getValue();
|
||||
assertTrue(sp instanceof BulkExportParameters);
|
||||
return (BulkExportParameters) sp;
|
||||
}
|
||||
|
||||
private Batch2JobStartResponse createJobStartResponse(String theJobId) {
|
||||
Batch2JobStartResponse response = new Batch2JobStartResponse();
|
||||
response.setJobId(theJobId);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private Batch2JobStartResponse createJobStartResponse() {
|
||||
return createJobStartResponse(A_JOB_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulInitiateBulkRequest_Post() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
String patientResource = "Patient";
|
||||
String practitionerResource = "Practitioner";
|
||||
String filter = "Patient?identifier=foo";
|
||||
when(myJobRunner.startNewJob(any()))
|
||||
.thenReturn(createJobStartResponse());
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
Parameters input = new Parameters();
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType("Patient, Practitioner"));
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType(patientResource + ", " + practitionerResource));
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_SINCE, now);
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE_FILTER, new StringType("Patient?identifier=foo"));
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE_FILTER, new StringType(filter));
|
||||
|
||||
ourLog.info(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
|
||||
|
||||
// test
|
||||
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
|
||||
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
post.setEntity(new ResourceEntity(myCtx, input));
|
||||
|
@ -141,21 +163,18 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), any(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Patient", "Practitioner"));
|
||||
assertThat(options.getSince(), notNullValue());
|
||||
assertThat(options.getFilters(), containsInAnyOrder("Patient?identifier=foo"));
|
||||
BulkExportParameters params = verifyJobStart();
|
||||
assertEquals(2, params.getResourceTypes().size());
|
||||
assertTrue(params.getResourceTypes().contains(patientResource));
|
||||
assertTrue(params.getResourceTypes().contains(practitionerResource));
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, params.getOutputFormat());
|
||||
assertNotNull(params.getStartDate());
|
||||
assertTrue(params.getFilters().contains(filter));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSuccessfulInitiateBulkRequest_Get() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(),any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse());
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
|
@ -176,21 +195,17 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), any(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Patient", "Practitioner"));
|
||||
assertThat(options.getSince(), notNullValue());
|
||||
assertThat(options.getFilters(), containsInAnyOrder("Patient?identifier=foo"));
|
||||
BulkExportParameters params = verifyJobStart();;
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, params.getOutputFormat());
|
||||
assertThat(params.getResourceTypes(), containsInAnyOrder("Patient", "Practitioner"));
|
||||
assertThat(params.getStartDate(), notNullValue());
|
||||
assertThat(params.getFilters(), containsInAnyOrder("Patient?identifier=foo"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSuccessfulInitiateBulkRequest_Get_MultipleTypeFilters() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(),any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
when(myJobRunner.startNewJob(any()))
|
||||
.thenReturn(createJobStartResponse());
|
||||
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
|
||||
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
|
||||
|
@ -209,23 +224,26 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), any(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Patient", "EpisodeOfCare"));
|
||||
assertThat(options.getSince(), nullValue());
|
||||
assertThat(options.getFilters(), containsInAnyOrder("Patient?_id=P999999990", "EpisodeOfCare?patient=P999999990"));
|
||||
BulkExportParameters params = verifyJobStart();;
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, params.getOutputFormat());
|
||||
assertThat(params.getResourceTypes(), containsInAnyOrder("Patient", "EpisodeOfCare"));
|
||||
assertThat(params.getStartDate(), nullValue());
|
||||
assertThat(params.getFilters(), containsInAnyOrder("Patient?_id=P999999990", "EpisodeOfCare?patient=P999999990"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollForStatus_BUILDING() throws IOException {
|
||||
public void testPollForStatus_QUEUED() throws IOException {
|
||||
// setup
|
||||
Batch2JobInfo info = new Batch2JobInfo();
|
||||
info.setJobId(A_JOB_ID);
|
||||
info.setStatus(BulkExportJobStatusEnum.BUILDING);
|
||||
info.setEndTime(new Date());
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID)
|
||||
.setStatus(BulkExportJobStatusEnum.BUILDING)
|
||||
.setStatusTime(InstantType.now().getValue());
|
||||
when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
|
||||
// when
|
||||
when(myJobRunner.getJobInfo(eq(A_JOB_ID)))
|
||||
.thenReturn(info);
|
||||
|
||||
// test
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
|
||||
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
|
||||
HttpGet get = new HttpGet(url);
|
||||
|
@ -236,21 +254,25 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals(202, response.getStatusLine().getStatusCode());
|
||||
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
|
||||
assertEquals("120", response.getFirstHeader(Constants.HEADER_RETRY_AFTER).getValue());
|
||||
assertThat(response.getFirstHeader(Constants.HEADER_X_PROGRESS).getValue(), containsString("Build in progress - Status set to BUILDING at 20"));
|
||||
assertThat(response.getFirstHeader(Constants.HEADER_X_PROGRESS).getValue(),
|
||||
containsString("Build in progress - Status set to " + info.getStatus() + " at 20"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollForStatus_ERROR() throws IOException {
|
||||
// setup
|
||||
Batch2JobInfo info = new Batch2JobInfo();
|
||||
info.setJobId(A_JOB_ID);
|
||||
info.setStatus(BulkExportJobStatusEnum.ERROR);
|
||||
info.setStartTime(new Date());
|
||||
info.setErrorMsg("Some Error Message");
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID)
|
||||
.setStatus(BulkExportJobStatusEnum.ERROR)
|
||||
.setStatusTime(InstantType.now().getValue())
|
||||
.setStatusMessage("Some Error Message");
|
||||
when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
|
||||
// when
|
||||
when(myJobRunner.getJobInfo(eq(A_JOB_ID)))
|
||||
.thenReturn(info);
|
||||
|
||||
// call
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
|
||||
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
|
||||
HttpGet get = new HttpGet(url);
|
||||
|
@ -265,21 +287,30 @@ public class BulkDataExportProviderTest {
|
|||
ourLog.info("Response content: {}", responseContent);
|
||||
assertThat(responseContent, containsString("\"diagnostics\": \"Some Error Message\""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollForStatus_COMPLETED() throws IOException {
|
||||
// setup
|
||||
Batch2JobInfo info = new Batch2JobInfo();
|
||||
info.setJobId(A_JOB_ID);
|
||||
info.setStatus(BulkExportJobStatusEnum.COMPLETE);
|
||||
info.setEndTime(InstantType.now().getValue());
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
ids.add(new IdType("Binary/111").getValueAsString());
|
||||
ids.add(new IdType("Binary/222").getValueAsString());
|
||||
ids.add(new IdType("Binary/333").getValueAsString());
|
||||
BulkExportJobResults results = new BulkExportJobResults();
|
||||
HashMap<String, List<String>> map = new HashMap<>();
|
||||
map.put("Patient", ids);
|
||||
results.setResourceTypeToBinaryIds(map);
|
||||
info.setReport(JsonUtil.serialize(results));
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID)
|
||||
.setStatus(BulkExportJobStatusEnum.COMPLETE)
|
||||
.setStatusTime(InstantType.now().getValue());
|
||||
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/111"));
|
||||
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/222"));
|
||||
jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/333"));
|
||||
when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo);
|
||||
// when
|
||||
when(myJobRunner.getJobInfo(eq(A_JOB_ID)))
|
||||
.thenReturn(info);
|
||||
|
||||
// call
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
|
||||
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
|
||||
HttpGet get = new HttpGet(url);
|
||||
|
@ -305,9 +336,51 @@ public class BulkDataExportProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testPollForStatus_Gone() throws IOException {
|
||||
public void testExportWhenNoResourcesReturned() throws IOException {
|
||||
// setup
|
||||
String msg = "Some msg";
|
||||
Batch2JobInfo info = new Batch2JobInfo();
|
||||
info.setJobId(A_JOB_ID);
|
||||
info.setStatus(BulkExportJobStatusEnum.COMPLETE);
|
||||
info.setEndTime(InstantType.now().getValue());
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
BulkExportJobResults results = new BulkExportJobResults();
|
||||
HashMap<String, List<String>> map = new HashMap<>();
|
||||
map.put("Patient", ids);
|
||||
results.setResourceTypeToBinaryIds(map);
|
||||
results.setReportMsg(msg);
|
||||
info.setReport(JsonUtil.serialize(results));
|
||||
|
||||
when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenThrow(new ResourceNotFoundException("Unknown job: AAA"));
|
||||
// when
|
||||
when(myJobRunner.getJobInfo(eq(A_JOB_ID)))
|
||||
.thenReturn(info);
|
||||
|
||||
// test
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
|
||||
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
|
||||
HttpGet get = new HttpGet(url);
|
||||
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
try (CloseableHttpResponse response = myClient.execute(get)) {
|
||||
ourLog.info("Response: {}", response.toString());
|
||||
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
assertEquals("OK", response.getStatusLine().getReasonPhrase());
|
||||
assertEquals(Constants.CT_JSON, response.getEntity().getContentType().getValue());
|
||||
|
||||
String responseContent = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
|
||||
ourLog.info("Response content: {}", responseContent);
|
||||
BulkExportResponseJson responseJson = JsonUtil.deserialize(responseContent, BulkExportResponseJson.class);
|
||||
assertEquals(msg, responseJson.getMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollForStatus_Gone() throws IOException {
|
||||
// setup
|
||||
|
||||
// when
|
||||
when(myJobRunner.getJobInfo(anyString()))
|
||||
.thenThrow(new ResourceNotFoundException("Unknown job: AAA"));
|
||||
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
|
||||
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
|
||||
|
@ -322,7 +395,6 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals(Constants.CT_FHIR_JSON_NEW, response.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
|
||||
assertThat(responseContent, containsString("\"diagnostics\":\"Unknown job: AAA\""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -336,14 +408,12 @@ public class BulkDataExportProviderTest {
|
|||
|
||||
@Test
|
||||
public void testSuccessfulInitiateGroupBulkRequest_Post() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo().setJobId(G_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(),any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
when(myBulkDataExportSvc.getPatientCompartmentResources()).thenReturn(Sets.newHashSet("Observation", "DiagnosticReport"));
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any()))
|
||||
.thenReturn(createJobStartResponse(G_JOB_ID));
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
|
||||
Parameters input = new Parameters();
|
||||
StringType obsTypeFilter = new StringType("Observation?code=OBSCODE,DiagnosticReport?code=DRCODE");
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
|
||||
|
@ -354,6 +424,7 @@ public class BulkDataExportProviderTest {
|
|||
|
||||
ourLog.info(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
|
||||
|
||||
// call
|
||||
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + GROUP_ID + "/" + JpaConstants.OPERATION_EXPORT);
|
||||
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
post.setEntity(new ResourceEntity(myCtx, input));
|
||||
|
@ -365,22 +436,21 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), any(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Observation", "DiagnosticReport"));
|
||||
assertThat(options.getSince(), notNullValue());
|
||||
assertThat(options.getFilters(), notNullValue());
|
||||
assertEquals(GROUP_ID, options.getGroupId().getValue());
|
||||
assertThat(options.isExpandMdm(), is(equalTo(true)));
|
||||
// verify
|
||||
BulkExportParameters bp = verifyJobStart();
|
||||
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, bp.getOutputFormat());
|
||||
assertThat(bp.getResourceTypes(), containsInAnyOrder("Observation", "DiagnosticReport"));
|
||||
assertThat(bp.getStartDate(), notNullValue());
|
||||
assertThat(bp.getFilters(), notNullValue());
|
||||
assertEquals(GROUP_ID, bp.getGroupId());
|
||||
assertThat(bp.isExpandMdm(), is(equalTo(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulInitiateGroupBulkRequest_Get() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo().setJobId(G_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
when(myBulkDataExportSvc.getPatientCompartmentResources()).thenReturn(Sets.newHashSet("Patient", "Practitioner"));
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse(G_JOB_ID));
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
|
@ -391,6 +461,7 @@ public class BulkDataExportProviderTest {
|
|||
+ "&" + JpaConstants.PARAM_EXPORT_TYPE_FILTER + "=" + UrlUtil.escapeUrlParam("Patient?identifier=foo|bar")
|
||||
+ "&" + JpaConstants.PARAM_EXPORT_MDM+ "=true";
|
||||
|
||||
// call
|
||||
HttpGet get = new HttpGet(url);
|
||||
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
ourLog.info("Request: {}", url);
|
||||
|
@ -402,24 +473,21 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), any(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Patient", "Practitioner"));
|
||||
assertThat(options.getSince(), notNullValue());
|
||||
assertThat(options.getFilters(), notNullValue());
|
||||
assertEquals(GROUP_ID, options.getGroupId().getValue());
|
||||
assertThat(options.isExpandMdm(), is(equalTo(true)));
|
||||
BulkExportParameters bp = verifyJobStart();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, bp.getOutputFormat());
|
||||
assertThat(bp.getResourceTypes(), containsInAnyOrder("Patient", "Practitioner"));
|
||||
assertThat(bp.getStartDate(), notNullValue());
|
||||
assertThat(bp.getFilters(), notNullValue());
|
||||
assertEquals(GROUP_ID, bp.getGroupId());
|
||||
assertThat(bp.isExpandMdm(), is(equalTo(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitiateWithGetAndMultipleTypeFilters() throws IOException {
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any())).thenReturn(jobInfo);
|
||||
|
||||
// setup
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
// manual construct
|
||||
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
|
||||
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
|
||||
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Immunization, Observation")
|
||||
|
@ -440,20 +508,20 @@ public class BulkDataExportProviderTest {
|
|||
|
||||
url += multiValuedTypeFilterBuilder.toString();
|
||||
|
||||
// call
|
||||
HttpGet get = new HttpGet(url);
|
||||
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
myClient.execute(get);
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), anyBoolean(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
|
||||
assertThat(options.getFilters(), containsInAnyOrder(immunizationTypeFilter1, immunizationTypeFilter2, observationFilter1));
|
||||
// verify
|
||||
BulkExportParameters bp = verifyJobStart();
|
||||
assertThat(bp.getFilters(), containsInAnyOrder(immunizationTypeFilter1, immunizationTypeFilter2, observationFilter1));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testInitiateGroupExportWithInvalidResourceTypesFails() throws IOException {
|
||||
when (myBulkDataExportSvc.getPatientCompartmentResources()).thenReturn(Sets.newHashSet("Observation"));
|
||||
// when
|
||||
|
||||
String url = "http://localhost:" + myPort + "/" + "Group/123/" +JpaConstants.OPERATION_EXPORT
|
||||
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
|
||||
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("StructureDefinition,Observation");
|
||||
|
@ -463,34 +531,34 @@ public class BulkDataExportProviderTest {
|
|||
CloseableHttpResponse execute = myClient.execute(get);
|
||||
String responseBody = IOUtils.toString(execute.getEntity().getContent());
|
||||
|
||||
// verify
|
||||
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(400)));
|
||||
assertThat(responseBody, is(containsString("Resource types [StructureDefinition] are invalid for this type of export, as they do not contain search parameters that refer to patients.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitiateGroupExportWithNoResourceTypes() throws IOException {
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
|
||||
.thenReturn(createJobStartResponse());
|
||||
|
||||
String url = "http://localhost:" + myPort + "/" + "Group/123/" +JpaConstants.OPERATION_EXPORT
|
||||
// test
|
||||
String url = "http://localhost:" + myPort + "/" + "Group/123/" + JpaConstants.OPERATION_EXPORT
|
||||
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON);
|
||||
|
||||
HttpGet get = new HttpGet(url);
|
||||
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
CloseableHttpResponse execute = myClient.execute(get);
|
||||
|
||||
// verify
|
||||
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(202)));
|
||||
verifyJobStart();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitiateWithPostAndMultipleTypeFilters() throws IOException {
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any())).thenReturn(createJobStartResponse());
|
||||
|
||||
Parameters input = new Parameters();
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
|
||||
|
@ -499,6 +567,7 @@ public class BulkDataExportProviderTest {
|
|||
|
||||
ourLog.info(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
|
||||
|
||||
// call
|
||||
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
|
||||
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
post.setEntity(new ResourceEntity(myCtx, input));
|
||||
|
@ -511,19 +580,18 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), myBooleanArgumentCaptor.capture(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Patient"));
|
||||
assertThat(options.getFilters(), containsInAnyOrder("Patient?gender=male", "Patient?gender=female"));
|
||||
// verify
|
||||
BulkExportParameters bp = verifyJobStart();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, bp.getOutputFormat());
|
||||
assertThat(bp.getResourceTypes(), containsInAnyOrder("Patient"));
|
||||
assertThat(bp.getFilters(), containsInAnyOrder("Patient?gender=male", "Patient?gender=female"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitiatePatientExportRequest() throws IOException {
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), any(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
when(myBulkDataExportSvc.getPatientCompartmentResources()).thenReturn(Sets.newHashSet("Immunization", "Observation"));
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any()))
|
||||
.thenReturn(createJobStartResponse());
|
||||
|
||||
InstantType now = InstantType.now();
|
||||
|
||||
|
@ -535,6 +603,7 @@ public class BulkDataExportProviderTest {
|
|||
|
||||
ourLog.info(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
|
||||
|
||||
// call
|
||||
HttpPost post = new HttpPost("http://localhost:" + myPort + "/Patient/" + JpaConstants.OPERATION_EXPORT);
|
||||
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
post.setEntity(new ResourceEntity(myCtx, input));
|
||||
|
@ -547,25 +616,28 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
verify(myBulkDataExportSvc, times(1)).submitJob(myBulkDataExportOptionsCaptor.capture(), myBooleanArgumentCaptor.capture(), nullable(RequestDetails.class));
|
||||
BulkDataExportOptions options = myBulkDataExportOptionsCaptor.getValue();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, options.getOutputFormat());
|
||||
assertThat(options.getResourceTypes(), containsInAnyOrder("Immunization", "Observation"));
|
||||
assertThat(options.getSince(), notNullValue());
|
||||
assertThat(options.getFilters(), containsInAnyOrder("Immunization?vaccine-code=foo"));
|
||||
BulkExportParameters bp = verifyJobStart();
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, bp.getOutputFormat());
|
||||
assertThat(bp.getResourceTypes(), containsInAnyOrder("Immunization", "Observation"));
|
||||
assertThat(bp.getStartDate(), notNullValue());
|
||||
assertThat(bp.getFilters(), containsInAnyOrder("Immunization?vaccine-code=foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProviderProcessesNoCacheHeader() throws IOException {
|
||||
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
|
||||
.setJobId(A_JOB_ID);
|
||||
when(myBulkDataExportSvc.submitJob(any(), anyBoolean(), nullable(RequestDetails.class))).thenReturn(jobInfo);
|
||||
// setup
|
||||
Batch2JobStartResponse startResponse = createJobStartResponse();
|
||||
startResponse.setUsesCachedResult(true);
|
||||
|
||||
// when
|
||||
when(myJobRunner.startNewJob(any(Batch2BaseJobParameters.class)))
|
||||
.thenReturn(startResponse);
|
||||
|
||||
Parameters input = new Parameters();
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
|
||||
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType("Patient, Practitioner"));
|
||||
|
||||
// call
|
||||
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
|
||||
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
|
||||
post.addHeader(Constants.HEADER_CACHE_CONTROL, Constants.CACHE_CONTROL_NO_CACHE);
|
||||
|
@ -578,10 +650,9 @@ public class BulkDataExportProviderTest {
|
|||
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
|
||||
}
|
||||
|
||||
|
||||
verify(myBulkDataExportSvc).submitJob(myBulkDataExportOptionsCaptor.capture(), myBooleanArgumentCaptor.capture(), nullable(RequestDetails.class));
|
||||
Boolean usedCache = myBooleanArgumentCaptor.getValue();
|
||||
assertThat(usedCache, is(equalTo(false)));
|
||||
// verify
|
||||
BulkExportParameters parameters = verifyJobStart();
|
||||
assertThat(parameters.isUseExistingJobsFirst(), is(equalTo(false)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
|
|||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
|
||||
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||
|
@ -91,7 +92,8 @@ public class BulkImportR4Test extends BaseJpaR4Test {
|
|||
|
||||
// Execute
|
||||
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
assertThat(instanceId, not(blankOrNullString()));
|
||||
ourLog.info("Execution got ID: {}", instanceId);
|
||||
|
||||
|
@ -144,7 +146,8 @@ public class BulkImportR4Test extends BaseJpaR4Test {
|
|||
|
||||
// Execute
|
||||
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
assertThat(instanceId, not(blankOrNullString()));
|
||||
ourLog.info("Execution got ID: {}", instanceId);
|
||||
|
||||
|
@ -215,8 +218,8 @@ public class BulkImportR4Test extends BaseJpaR4Test {
|
|||
request.setParameters(parameters);
|
||||
|
||||
// Execute
|
||||
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
assertThat(instanceId, not(blankOrNullString()));
|
||||
ourLog.info("Execution got ID: {}", instanceId);
|
||||
|
||||
|
@ -258,8 +261,8 @@ public class BulkImportR4Test extends BaseJpaR4Test {
|
|||
try {
|
||||
|
||||
// Execute
|
||||
|
||||
String instanceId = myJobCoordinator.startInstance(request);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request);
|
||||
String instanceId = startResponse.getJobId();
|
||||
assertThat(instanceId, not(blankOrNullString()));
|
||||
ourLog.info("Execution got ID: {}", instanceId);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ 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.JobInstanceStartRequest;
|
||||
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;
|
||||
|
@ -63,8 +64,8 @@ public class DeleteExpungeJobTest extends BaseJpaR4Test {
|
|||
startRequest.setJobDefinitionId(DeleteExpungeAppCtx.JOB_DELETE_EXPUNGE);
|
||||
|
||||
// execute
|
||||
String jobId = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitJobCompletion(jobId);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitJobCompletion(startResponse);
|
||||
|
||||
// validate
|
||||
assertEquals(1, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
|
||||
|
|
|
@ -8,6 +8,7 @@ import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
|
|||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
@ -61,8 +62,8 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
||||
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||
startRequest.setParameters(parameters);
|
||||
String id = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(id);
|
||||
Batch2JobStartResponse res = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(res);
|
||||
|
||||
// validate
|
||||
assertEquals(2, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
|
||||
|
@ -94,8 +95,8 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
||||
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||
startRequest.setParameters(new ReindexJobParameters());
|
||||
String id = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(id);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
|
||||
myBatch2JobHelper.awaitSingleChunkJobCompletion(startResponse);
|
||||
|
||||
// validate
|
||||
assertEquals(50, myObservationDao.search(SearchParameterMap.newSynchronous()).size());
|
||||
|
@ -123,8 +124,8 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
||||
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||
startRequest.setParameters(new ReindexJobParameters());
|
||||
String id = myJobCoordinator.startInstance(startRequest);
|
||||
JobInstance outcome = myBatch2JobHelper.awaitJobFailure(id);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
|
||||
JobInstance outcome = myBatch2JobHelper.awaitJobFailure(startResponse);
|
||||
|
||||
// Verify
|
||||
|
||||
|
@ -151,8 +152,8 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
|||
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
||||
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||
startRequest.setParameters(new ReindexJobParameters());
|
||||
String id = myJobCoordinator.startInstance(startRequest);
|
||||
JobInstance outcome = myBatch2JobHelper.awaitJobFailure(id);
|
||||
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(startRequest);
|
||||
JobInstance outcome = myBatch2JobHelper.awaitJobFailure(startResponse);
|
||||
|
||||
// Verify
|
||||
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
package ca.uhn.fhir.jpa.interceptor;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
import ca.uhn.fhir.jpa.api.svc.IBatch2JobRunner;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
|
||||
import ca.uhn.fhir.jpa.util.BulkExportUtils;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Binary;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -24,11 +22,12 @@ import org.junit.jupiter.api.Test;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -40,19 +39,11 @@ public class ResponseTerminologyTranslationInterceptorTest extends BaseResourceP
|
|||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseTerminologyTranslationInterceptorTest.class);
|
||||
public static final String TEST_OBV_FILTER = "Observation?status=amended";
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
@Autowired
|
||||
private ResponseTerminologyTranslationInterceptor myResponseTerminologyTranslationInterceptor;
|
||||
@Autowired
|
||||
private IBulkDataExportSvc myBulkDataExportSvc;
|
||||
@Autowired
|
||||
private IBulkDataExportJobSchedulingHelper myBulkDataExportJobSchedulingHelper;
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
private IBatch2JobRunner myJobRunner;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
|
@ -204,53 +195,42 @@ public class ResponseTerminologyTranslationInterceptorTest extends BaseResourceP
|
|||
options.setResourceTypes(Sets.newHashSet("Observation"));
|
||||
options.setFilters(Sets.newHashSet(TEST_OBV_FILTER));
|
||||
options.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(options, true, mySrd);
|
||||
assertNotNull(jobDetails.getJobId());
|
||||
Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(options));
|
||||
|
||||
// Check the status
|
||||
IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId());
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus());
|
||||
assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation&_typeFilter=" + UrlUtil.escapeUrlParam(TEST_OBV_FILTER), status.getRequest());
|
||||
assertNotNull(startResponse);
|
||||
|
||||
// Run a scheduled pass to build the export
|
||||
myBulkDataExportJobSchedulingHelper.startSubmittedJobs();
|
||||
awaitAllBulkJobCompletions();
|
||||
myBatch2JobHelper.awaitJobCompletion(startResponse.getJobId());
|
||||
|
||||
// Fetch the job again
|
||||
status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId());
|
||||
assertEquals(BulkExportJobStatusEnum.COMPLETE, status.getStatus());
|
||||
await().until(() -> myJobRunner.getJobInfo(startResponse.getJobId()).getReport() != null);
|
||||
|
||||
// Iterate over the files
|
||||
for (IBulkDataExportSvc.FileEntry next : status.getFiles()) {
|
||||
Binary nextBinary = myBinaryDao.read(next.getResourceId());
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType());
|
||||
String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8);
|
||||
ourLog.info("Next contents for type {}:\n{}", next.getResourceType(), nextContents);
|
||||
if ("Observation".equals(next.getResourceType())) {
|
||||
for (String coding : codingList) {
|
||||
assertThat(nextContents, containsString(coding));
|
||||
String report = myJobRunner.getJobInfo(startResponse.getJobId()).getReport();
|
||||
BulkExportJobResults results = JsonUtil.deserialize(report, BulkExportJobResults.class);
|
||||
for (Map.Entry<String, List<String>> file : results.getResourceTypeToBinaryIds().entrySet()) {
|
||||
String resourceTypeInFile = file.getKey();
|
||||
List<String> binaryIds = file.getValue();
|
||||
assertEquals(1, binaryIds.size());
|
||||
for (String binaryId : binaryIds) {
|
||||
Binary binary = myBinaryDao.read(new IdType(binaryId));
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, binary.getContentType());
|
||||
String contents = new String(binary.getContent(), Constants.CHARSET_UTF8);
|
||||
ourLog.info("Next contents for type {} :\n{}", binary.getResourceType(), contents);
|
||||
if ("Observation".equals(resourceTypeInFile)) {
|
||||
for (String code : codingList) {
|
||||
assertThat(contents, containsString(code));
|
||||
}
|
||||
} else {
|
||||
fail(next.getResourceType());
|
||||
fail(resourceTypeInFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(1, status.getFiles().size());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<String> toCodeStrings(Observation observation) {
|
||||
return observation.getCode().getCoding().stream().map(t -> "[system=" + t.getSystem() + ", code=" + t.getCode() + ", display=" + t.getDisplay() + "]").collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void awaitAllBulkJobCompletions() {
|
||||
myBatchJobHelper.awaitAllBulkJobCompletions(
|
||||
BatchConstants.BULK_EXPORT_JOB_NAME,
|
||||
BatchConstants.PATIENT_BULK_EXPORT_JOB_NAME,
|
||||
BatchConstants.GROUP_BULK_EXPORT_JOB_NAME,
|
||||
BatchConstants.DELETE_EXPUNGE_JOB_NAME,
|
||||
BatchConstants.MDM_CLEAR_JOB_NAME
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
|
||||
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest;
|
||||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||
|
@ -14,7 +12,6 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
|
||||
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
|
@ -25,10 +22,7 @@ import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleTester;
|
|||
import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
|
||||
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.github.jsonldjava.shaded.com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
|
@ -61,7 +55,6 @@ import org.junit.jupiter.api.Test;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
@ -80,9 +73,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
|||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorJpaR4Test.class);
|
||||
|
||||
@Autowired
|
||||
private IBulkDataExportSvc myBulkDataExportSvc;
|
||||
|
||||
@Autowired
|
||||
private SearchParamMatcher mySearchParamMatcher;
|
||||
|
||||
|
@ -103,297 +93,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
|||
myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof AuthorizationInterceptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkExport_AuthorizeGroupId() {
|
||||
|
||||
AuthorizationInterceptor authInterceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow().bulkExport().groupExportOnGroup(new IdType("Group/123")).andThen()
|
||||
.allow().bulkExport().groupExportOnGroup(new IdType("Group/789")).andThen()
|
||||
.build();
|
||||
}
|
||||
};
|
||||
myInterceptorRegistry.registerInterceptor(authInterceptor);
|
||||
|
||||
/*
|
||||
* Matching group ID
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/789"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
/*
|
||||
* Second matching group ID
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/789"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Non matching group ID
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Non group export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBulkExport_AuthorizePatientId() {
|
||||
|
||||
AuthorizationInterceptor authInterceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow().bulkExport().patientExportOnGroup(new IdType("Group/123")).andThen()
|
||||
.build();
|
||||
}
|
||||
};
|
||||
myInterceptorRegistry.registerInterceptor(authInterceptor);
|
||||
|
||||
/*
|
||||
* Matching group ID
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/123"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
/*
|
||||
* Non matching group ID
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Non group export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBulkExport_AuthorizeSystem() {
|
||||
|
||||
AuthorizationInterceptor authInterceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow().bulkExport().systemExport().andThen()
|
||||
.build();
|
||||
}
|
||||
};
|
||||
myInterceptorRegistry.registerInterceptor(authInterceptor);
|
||||
|
||||
/*
|
||||
* System export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
/*
|
||||
* Patient export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBulkExport_AuthorizeAny() {
|
||||
|
||||
AuthorizationInterceptor authInterceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow().bulkExport().any().andThen()
|
||||
.build();
|
||||
}
|
||||
};
|
||||
myInterceptorRegistry.registerInterceptor(authInterceptor);
|
||||
|
||||
/*
|
||||
* System export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
/*
|
||||
* Patient export
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setGroupId(new IdType("Group/456"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkExport_SpecificResourceTypesEnforced() {
|
||||
|
||||
AuthorizationInterceptor authInterceptor = new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow().bulkExport().systemExport().withResourceTypes(Lists.newArrayList("Patient", "Encounter")).andThen()
|
||||
.build();
|
||||
}
|
||||
};
|
||||
myInterceptorRegistry.registerInterceptor(authInterceptor);
|
||||
|
||||
/*
|
||||
* Appropriate Resources
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setResourceTypes(Sets.newHashSet("Patient", "Encounter"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
assertEquals(BulkExportJobStatusEnum.SUBMITTED, jobDetails.getStatus());
|
||||
}
|
||||
|
||||
/*
|
||||
* Inappropriate Resources
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setResourceTypes(Sets.newHashSet("Patient", "Encounter", "Observation"));
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* No Resources
|
||||
*/
|
||||
{
|
||||
BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions();
|
||||
bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
try {
|
||||
ServletRequestDetails requestDetails = new ServletRequestDetails().setServletRequest(new MockHttpServletRequest());
|
||||
myBulkDataExportSvc.submitJob(bulkDataExportOptions, true, requestDetails);
|
||||
fail();
|
||||
} catch (ForbiddenOperationException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* See #667
|
||||
*/
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
|
||||
import ca.uhn.fhir.jpa.api.model.Batch2JobInfo;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
import ca.uhn.fhir.jpa.api.svc.IBatch2JobRunner;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson;
|
||||
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
|
||||
import ca.uhn.fhir.jpa.entity.PartitionEntity;
|
||||
|
@ -11,11 +14,14 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
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.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
|
@ -24,37 +30,51 @@ import org.apache.http.client.methods.CloseableHttpResponse;
|
|||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.Condition;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
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.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Test implements ITestDataBuilder {
|
||||
|
||||
@Autowired
|
||||
private IBulkDataExportSvc myBulkDataExportSvc;
|
||||
@Autowired
|
||||
private IBulkDataExportJobSchedulingHelper myBulkDataExportJobSchedulingHelper;
|
||||
|
||||
|
@ -587,6 +607,23 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
assertThat(response.getEntry(), hasSize(2));
|
||||
}
|
||||
|
||||
private void createConditionWithAllowedUnqualified(IIdType idA) {
|
||||
myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED);
|
||||
IIdType idB = createResource("Condition", withTenant(TENANT_A), withObservationCode("http://cs", "A"));
|
||||
Condition theCondition = myClient.read().resource(Condition.class).withId(idB).execute();
|
||||
theCondition.getSubject().setReference("Patient/" + idA.getIdPart());
|
||||
doUpdateResource(theCondition);
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class PartitionTesting {
|
||||
|
||||
@InjectMocks
|
||||
private BulkDataExportProvider myProvider;
|
||||
|
||||
@Mock
|
||||
private IBatch2JobRunner myJobRunner;
|
||||
|
||||
@Test
|
||||
public void testBulkExportForDifferentPartitions() throws IOException {
|
||||
setBulkDataExportProvider();
|
||||
|
@ -596,28 +633,55 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
}
|
||||
|
||||
private void testBulkExport(String createInPartition) throws IOException {
|
||||
// Create a patient
|
||||
IBaseResource patientA = buildPatient(withActiveTrue());
|
||||
SystemRequestDetails requestDetails = new SystemRequestDetails();
|
||||
requestDetails.setTenantId(createInPartition);
|
||||
myPatientDao.create((Patient) patientA, requestDetails);
|
||||
// setup
|
||||
String jobId = "jobId";
|
||||
RestfulServer mockServer = mock(RestfulServer.class);
|
||||
HttpServletResponse mockResponse = mock(HttpServletResponse.class);
|
||||
|
||||
BulkExportJobResults results = new BulkExportJobResults();
|
||||
HashMap<String, List<String>> map = new HashMap<>();
|
||||
map.put("Patient", Arrays.asList("Binary/1", "Binary/2"));
|
||||
results.setResourceTypeToBinaryIds(map);
|
||||
|
||||
Batch2JobInfo jobInfo = new Batch2JobInfo();
|
||||
jobInfo.setJobId(jobId);
|
||||
jobInfo.setStatus(BulkExportJobStatusEnum.COMPLETE);
|
||||
jobInfo.setReport(JsonUtil.serialize(results));
|
||||
|
||||
// Create a bulk job
|
||||
BulkDataExportOptions options = new BulkDataExportOptions();
|
||||
options.setResourceTypes(Sets.newHashSet("Patient"));
|
||||
options.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM);
|
||||
|
||||
IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(options, false, requestDetails);
|
||||
assertNotNull(jobDetails.getJobId());
|
||||
Batch2JobStartResponse startResponse = new Batch2JobStartResponse();
|
||||
startResponse.setJobId(jobId);
|
||||
when(myJobRunner.startNewJob(any()))
|
||||
.thenReturn(startResponse);
|
||||
when(myJobRunner.getJobInfo(anyString()))
|
||||
.thenReturn(jobInfo);
|
||||
|
||||
// Run a scheduled pass to build the export and wait for completion
|
||||
myBulkDataExportJobSchedulingHelper.startSubmittedJobs();
|
||||
myBatchJobHelper.awaitAllBulkJobCompletions(
|
||||
BatchConstants.BULK_EXPORT_JOB_NAME
|
||||
);
|
||||
// mocking
|
||||
ServletRequestDetails servletRequestDetails = spy(new ServletRequestDetails());
|
||||
MockHttpServletRequest reqDetails = new MockHttpServletRequest();
|
||||
reqDetails.addHeader(Constants.HEADER_PREFER,
|
||||
"respond-async");
|
||||
servletRequestDetails.setServletRequest(reqDetails);
|
||||
doReturn(JpaConstants.DEFAULT_PARTITION_NAME + "/")
|
||||
.when(servletRequestDetails).getServerBaseForRequest();
|
||||
when(servletRequestDetails.getServer())
|
||||
.thenReturn(mockServer);
|
||||
when(servletRequestDetails.getServletResponse())
|
||||
.thenReturn(mockResponse);
|
||||
|
||||
List<IPrimitiveType<String>> filters = new ArrayList<>();
|
||||
if (options.getFilters() != null) {
|
||||
for (String v : options.getFilters()) {
|
||||
filters.add(new StringType(v));
|
||||
}
|
||||
}
|
||||
|
||||
//perform export-poll-status
|
||||
HttpGet get = new HttpGet(buildExportUrl(createInPartition, jobDetails.getJobId()));
|
||||
HttpGet get = new HttpGet(buildExportUrl(createInPartition, jobId));
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
BulkExportResponseJson responseJson = JsonUtil.deserialize(responseString, BulkExportResponseJson.class);
|
||||
|
@ -625,23 +689,14 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
private void setBulkDataExportProvider() {
|
||||
BulkDataExportProvider provider = new BulkDataExportProvider();
|
||||
provider.setBulkDataExportSvcForUnitTests(myBulkDataExportSvc);
|
||||
provider.setFhirContextForUnitTest(myFhirContext);
|
||||
ourRestServer.registerProvider(provider);
|
||||
ourRestServer.registerProvider(myProvider);
|
||||
}
|
||||
|
||||
private String buildExportUrl(String createInPartition, String jobId) {
|
||||
return myClient.getServerBase() + "/" + createInPartition + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?"
|
||||
+ JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + jobId;
|
||||
}
|
||||
|
||||
private void createConditionWithAllowedUnqualified(IIdType idA) {
|
||||
myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED);
|
||||
IIdType idB = createResource("Condition", withTenant(TENANT_A), withObservationCode("http://cs", "A"));
|
||||
Condition theCondition = myClient.read().resource(Condition.class).withId(idB).execute();
|
||||
theCondition.getSubject().setReference("Patient/" + idA.getIdPart());
|
||||
doUpdateResource(theCondition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package ca.uhn.fhir.jpa.testutil;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportParameters;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import ca.uhn.fhir.util.Batch2JobDefinitionConstants;
|
||||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BulkExportBatch2TestUtils {
|
||||
|
||||
|
||||
public static BulkExportParameters getBulkExportParametersFromOptions(FhirContext theCtx,
|
||||
BulkDataExportOptions theOptions) {
|
||||
BulkExportParameters parameters = new BulkExportParameters(Batch2JobDefinitionConstants.BULK_EXPORT);
|
||||
|
||||
parameters.setStartDate(theOptions.getSince());
|
||||
parameters.setOutputFormat(theOptions.getOutputFormat());
|
||||
parameters.setExportStyle(theOptions.getExportStyle());
|
||||
if (theOptions.getFilters() != null) {
|
||||
parameters.setFilters(new ArrayList<>(theOptions.getFilters()));
|
||||
}
|
||||
if (theOptions.getGroupId() != null) {
|
||||
parameters.setGroupId(theOptions.getGroupId().getValue());
|
||||
}
|
||||
parameters.setExpandMdm(theOptions.isExpandMdm());
|
||||
|
||||
// resource types are special
|
||||
// if none are provided, the job submitter adds them
|
||||
// but we cannot manually start the job without correct parameters
|
||||
// so we "correct" them here
|
||||
if (theOptions.getResourceTypes() == null || theOptions.getResourceTypes().isEmpty()) {
|
||||
addAllResourceTypes(parameters, theCtx);
|
||||
}
|
||||
else {
|
||||
parameters.setResourceTypes(new ArrayList<>(theOptions.getResourceTypes()));
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static void addAllResourceTypes(BulkExportParameters theOptions, FhirContext theCtx) {
|
||||
Set<String> rts = theCtx.getResourceTypes();
|
||||
if (theOptions.getExportStyle() == BulkDataExportOptions.ExportStyle.SYSTEM) {
|
||||
// everything
|
||||
List<String> resourceTypes = rts.stream()
|
||||
.filter(rt -> !rt.equalsIgnoreCase("Binary"))
|
||||
.collect(Collectors.toList());
|
||||
theOptions.setResourceTypes(resourceTypes);
|
||||
}
|
||||
else if (theOptions.getExportStyle() != null) {
|
||||
// patients
|
||||
List<String> patientRts = rts.stream()
|
||||
.filter(rt -> SearchParameterUtil.isResourceTypeInPatientCompartment(theCtx, rt))
|
||||
.collect(Collectors.toList());
|
||||
theOptions.setResourceTypes(patientRts);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ public class BulkDataExportOptions {
|
|||
GROUP,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
private String myOutputFormat;
|
||||
private Set<String> myResourceTypes;
|
||||
private Date mySince;
|
||||
|
@ -43,8 +44,6 @@ public class BulkDataExportOptions {
|
|||
private boolean myExpandMdm;
|
||||
private IIdType myGroupId;
|
||||
|
||||
|
||||
|
||||
public void setOutputFormat(String theOutputFormat) {
|
||||
myOutputFormat = theOutputFormat;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
@ -41,47 +41,6 @@
|
|||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<profiles>
|
||||
<!-- For releases, we need to generate javadoc and sources JAR -->
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<activation>
|
||||
<activeByDefault>false</activeByDefault>
|
||||
<property>
|
||||
<name>deployToSonatype</name>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.1.0-PRE14-SNAPSHOT</version>
|
||||
<version>6.1.0-PRE15-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -20,6 +20,7 @@ package ca.uhn.fhir.batch2.jobs.config;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.jobs.export.BulkExportAppCtx;
|
||||
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeAppCtx;
|
||||
import ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx;
|
||||
import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx;
|
||||
|
@ -32,8 +33,8 @@ import org.springframework.context.annotation.Import;
|
|||
BatchCommonCtx.class,
|
||||
BulkImportAppCtx.class,
|
||||
ReindexAppCtx.class,
|
||||
DeleteExpungeAppCtx.class
|
||||
DeleteExpungeAppCtx.class,
|
||||
BulkExportAppCtx.class
|
||||
})
|
||||
public class Batch2JobsConfig {
|
||||
// nothing
|
||||
}
|
||||
|
|
|
@ -1,6 +1,28 @@
|
|||
package ca.uhn.fhir.batch2.jobs.config;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.jobs.parameters.UrlPartitioner;
|
||||
import ca.uhn.fhir.batch2.jobs.services.Batch2JobRunnerImpl;
|
||||
import ca.uhn.fhir.jpa.api.svc.IBatch2JobRunner;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -10,4 +32,9 @@ public class BatchCommonCtx {
|
|||
UrlPartitioner urlPartitioner(MatchUrlService theMatchUrlService, IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
|
||||
return new UrlPartitioner(theMatchUrlService, theRequestPartitionHelperSvc);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IBatch2JobRunner batch2JobRunner() {
|
||||
return new Batch2JobRunnerImpl();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.api.VoidModel;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportBinaryFileId;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportExpandedResources;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportIdList;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.batch2.model.JobDefinition;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import ca.uhn.fhir.util.Batch2JobDefinitionConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
@Configuration
|
||||
public class BulkExportAppCtx {
|
||||
|
||||
@Bean
|
||||
public JobDefinition bulkExportJobDefinition() {
|
||||
JobDefinition.Builder<IModelJson, VoidModel> builder = JobDefinition.newBuilder();
|
||||
builder.setJobDefinitionId(Batch2JobDefinitionConstants.BULK_EXPORT);
|
||||
builder.setJobDescription("FHIR Bulk Export");
|
||||
builder.setJobDefinitionVersion(1);
|
||||
|
||||
JobDefinition def = builder.setParametersType(BulkExportJobParameters.class)
|
||||
// validator
|
||||
.setParametersValidator(bulkExportJobParametersValidator())
|
||||
.gatedExecution()
|
||||
// first step - load in (all) ids and create id chunks of 1000 each
|
||||
.addFirstStep(
|
||||
"fetch-resources",
|
||||
"Fetches resource PIDs for exporting",
|
||||
BulkExportIdList.class,
|
||||
fetchResourceIdsStep()
|
||||
)
|
||||
// expand out - fetch resources
|
||||
.addIntermediateStep(
|
||||
"expand-resources",
|
||||
"Expand out resources",
|
||||
BulkExportExpandedResources.class,
|
||||
expandResourcesStep()
|
||||
)
|
||||
// write binaries and save to db
|
||||
.addIntermediateStep(
|
||||
"write-to-binaries",
|
||||
"Writes the expanded resources to the binaries and saves",
|
||||
BulkExportBinaryFileId.class,
|
||||
writeBinaryStep()
|
||||
)
|
||||
// finalize the job (set to complete)
|
||||
.addFinalReducerStep(
|
||||
"create-report-step",
|
||||
"Creates the output report from a bulk export job",
|
||||
BulkExportJobResults.class,
|
||||
createReportStep()
|
||||
)
|
||||
.build();
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BulkExportJobParametersValidator bulkExportJobParametersValidator() {
|
||||
return new BulkExportJobParametersValidator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FetchResourceIdsStep fetchResourceIdsStep() {
|
||||
return new FetchResourceIdsStep();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ExpandResourcesStep expandResourcesStep() {
|
||||
return new ExpandResourcesStep();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WriteBinaryStep writeBinaryStep() {
|
||||
return new WriteBinaryStep();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public BulkExportCreateReportStep createReportStep() {
|
||||
return new BulkExportCreateReportStep();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.api.ChunkExecutionDetails;
|
||||
import ca.uhn.fhir.batch2.api.IJobDataSink;
|
||||
import ca.uhn.fhir.batch2.api.IReductionStepWorker;
|
||||
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.jobs.export.models.BulkExportBinaryFileId;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.batch2.model.ChunkOutcome;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
public class BulkExportCreateReportStep implements IReductionStepWorker<BulkExportJobParameters, BulkExportBinaryFileId, BulkExportJobResults> {
|
||||
private static final Logger ourLog = getLogger(BulkExportCreateReportStep.class);
|
||||
|
||||
private Map<String, List<String>> myResourceToBinaryIds;
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public RunOutcome run(@NotNull StepExecutionDetails<BulkExportJobParameters, BulkExportBinaryFileId> theStepExecutionDetails,
|
||||
@NotNull IJobDataSink<BulkExportJobResults> theDataSink) throws JobExecutionFailedException {
|
||||
BulkExportJobResults results = new BulkExportJobResults();
|
||||
|
||||
if (myResourceToBinaryIds != null) {
|
||||
ourLog.info("Bulk Export Report creation step");
|
||||
|
||||
results.setResourceTypeToBinaryIds(myResourceToBinaryIds);
|
||||
|
||||
myResourceToBinaryIds = null;
|
||||
} else {
|
||||
String msg = "Export complete, but no data to generate report.";
|
||||
ourLog.warn(msg);
|
||||
|
||||
results.setReportMsg(msg);
|
||||
}
|
||||
|
||||
// accept saves the report
|
||||
theDataSink.accept(results);
|
||||
return RunOutcome.SUCCESS;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ChunkOutcome consume(ChunkExecutionDetails<BulkExportJobParameters,
|
||||
BulkExportBinaryFileId> theChunkDetails) {
|
||||
BulkExportBinaryFileId fileId = theChunkDetails.getData();
|
||||
if (myResourceToBinaryIds == null) {
|
||||
myResourceToBinaryIds = new HashMap<>();
|
||||
}
|
||||
|
||||
myResourceToBinaryIds.putIfAbsent(fileId.getResourceType(), new ArrayList<>());
|
||||
|
||||
myResourceToBinaryIds.get(fileId.getResourceType()).add(fileId.getBinaryId());
|
||||
|
||||
return ChunkOutcome.SUCCESS();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.api.IJobParametersValidator;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BulkExportJobParametersValidator implements IJobParametersValidator<BulkExportJobParameters> {
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> validate(@Nonnull BulkExportJobParameters theParameters) {
|
||||
List<String> errorMsgs = new ArrayList<>();
|
||||
|
||||
// initial validation
|
||||
|
||||
if (theParameters.getResourceTypes() == null || theParameters.getResourceTypes().isEmpty()) {
|
||||
errorMsgs.add("Resource Types are required for an export job.");
|
||||
}
|
||||
else {
|
||||
for (String resourceType : theParameters.getResourceTypes()) {
|
||||
if (resourceType.equalsIgnoreCase("Binary")) {
|
||||
errorMsgs.add("Bulk export of Binary resources is forbidden");
|
||||
} else if (!myDaoRegistry.isResourceTypeSupported(resourceType)) {
|
||||
errorMsgs.add("Resource type " + resourceType + " is not a supported resource type!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate the output format
|
||||
if (!Constants.CT_FHIR_NDJSON.equalsIgnoreCase(theParameters.getOutputFormat())) {
|
||||
errorMsgs.add("The only allowed format for Bulk Export is currently " + Constants.CT_FHIR_NDJSON);
|
||||
}
|
||||
|
||||
// validate for group
|
||||
BulkDataExportOptions.ExportStyle style = theParameters.getExportStyle();
|
||||
if (style == null) {
|
||||
errorMsgs.add("Export style is required");
|
||||
}
|
||||
else {
|
||||
switch (style) {
|
||||
case GROUP:
|
||||
if (theParameters.getGroupId() == null || theParameters.getGroupId().isEmpty()) {
|
||||
errorMsgs.add("Group export requires a group id, but none provided.");
|
||||
}
|
||||
break;
|
||||
case SYSTEM:
|
||||
case PATIENT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return errorMsgs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
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.jobs.export.models.BulkExportExpandedResources;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportIdList;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.batch2.jobs.models.Id;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkExportProcessor;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationSvc;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
public class ExpandResourcesStep implements IJobStepWorker<BulkExportJobParameters, BulkExportIdList, BulkExportExpandedResources> {
|
||||
private static final Logger ourLog = getLogger(ExpandResourcesStep.class);
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
@Autowired
|
||||
private IBulkExportProcessor myBulkExportProcessor;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext myApplicationContext;
|
||||
|
||||
private ResponseTerminologyTranslationSvc myResponseTerminologyTranslationSvc;
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
boolean includeNonSingletons = false;
|
||||
// We don't want to init lazy loaded beans here, so we call getBeansOfType with allowEagerInit = false
|
||||
boolean allowEagerInit = false;
|
||||
try {
|
||||
Map<String, ResponseTerminologyTranslationSvc> rttMap = myApplicationContext.getBeansOfType(ResponseTerminologyTranslationSvc.class, includeNonSingletons, allowEagerInit);
|
||||
Optional<ResponseTerminologyTranslationSvc> oRtt = rttMap.values().stream().findFirst();
|
||||
if (oRtt.isPresent()) {
|
||||
myResponseTerminologyTranslationSvc = oRtt.get();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ourLog.info("Terminology Translation not available on this server. Bulk Export will operate without it.");
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RunOutcome run(@Nonnull StepExecutionDetails<BulkExportJobParameters, BulkExportIdList> theStepExecutionDetails,
|
||||
@Nonnull IJobDataSink<BulkExportExpandedResources> theDataSink) throws JobExecutionFailedException {
|
||||
BulkExportIdList idList = theStepExecutionDetails.getData();
|
||||
BulkExportJobParameters jobParameters = theStepExecutionDetails.getParameters();
|
||||
|
||||
ourLog.info("Step 2 for bulk export - Expand resources");
|
||||
|
||||
// search the resources
|
||||
IBundleProvider bundle = fetchAllResources(idList);
|
||||
|
||||
List<IBaseResource> allResources = bundle.getAllResources();
|
||||
|
||||
// if necessary, expand resources
|
||||
if (jobParameters.isExpandMdm()) {
|
||||
myBulkExportProcessor.expandMdmResources(allResources);
|
||||
}
|
||||
|
||||
if (myResponseTerminologyTranslationSvc != null) {
|
||||
myResponseTerminologyTranslationSvc.processResourcesForTerminologyTranslation(allResources);
|
||||
}
|
||||
|
||||
// encode them
|
||||
List<String> resources = encodeToString(allResources, jobParameters);
|
||||
|
||||
// set to datasink
|
||||
BulkExportExpandedResources output = new BulkExportExpandedResources();
|
||||
output.setStringifiedResources(resources);
|
||||
output.setResourceType(idList.getResourceType());
|
||||
theDataSink.accept(output);
|
||||
|
||||
ourLog.trace("Expanding of {} resources of type {} completed",
|
||||
idList.getIds().size(),
|
||||
idList.getResourceType());
|
||||
|
||||
// and return
|
||||
return RunOutcome.SUCCESS;
|
||||
}
|
||||
|
||||
private IBundleProvider fetchAllResources(BulkExportIdList theIds) {
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theIds.getResourceType());
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
TokenOrListParam ids = new TokenOrListParam();
|
||||
for (Id id : theIds.getIds()) {
|
||||
ids.addOr(new TokenParam(id.toPID().getAssociatedResourceId().getValue()));
|
||||
}
|
||||
map.add(Constants.PARAM_ID, ids);
|
||||
return dao.search(map, SystemRequestDetails.forAllPartitions());
|
||||
}
|
||||
|
||||
private List<String> encodeToString(List<IBaseResource> theResources, BulkExportJobParameters theParameters) {
|
||||
IParser parser = getParser(theParameters);
|
||||
|
||||
List<String> resources = new ArrayList<>();
|
||||
for (IBaseResource resource : theResources) {
|
||||
String jsonResource = parser.encodeResourceToString(resource);
|
||||
resources.add(jsonResource);
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
private IParser getParser(BulkExportJobParameters theParameters) {
|
||||
// The parser depends on the
|
||||
// output format
|
||||
// (but for now, only ndjson is supported
|
||||
// see WriteBinaryStep as well
|
||||
return myFhirContext.newJsonParser().setPrettyPrint(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.api.IFirstJobStepWorker;
|
||||
import ca.uhn.fhir.batch2.api.IJobDataSink;
|
||||
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.jobs.export.models.BulkExportIdList;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.batch2.jobs.models.Id;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkExportProcessor;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.ExportPIDIteratorParameters;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class FetchResourceIdsStep implements IFirstJobStepWorker<BulkExportJobParameters, BulkExportIdList> {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(FetchResourceIdsStep.class);
|
||||
|
||||
public static final int MAX_IDS_TO_BATCH = 1000;
|
||||
|
||||
@Autowired
|
||||
private IBulkExportProcessor myBulkExportProcessor;
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RunOutcome run(@Nonnull StepExecutionDetails<BulkExportJobParameters, VoidModel> theStepExecutionDetails,
|
||||
@Nonnull IJobDataSink<BulkExportIdList> theDataSink) throws JobExecutionFailedException {
|
||||
BulkExportJobParameters params = theStepExecutionDetails.getParameters();
|
||||
ourLog.info("Starting BatchExport job");
|
||||
|
||||
ExportPIDIteratorParameters providerParams = new ExportPIDIteratorParameters();
|
||||
providerParams.setFilters(params.getFilters());
|
||||
providerParams.setStartDate(params.getStartDate());
|
||||
providerParams.setExportStyle(params.getExportStyle());
|
||||
providerParams.setGroupId(params.getGroupId());
|
||||
providerParams.setExpandMdm(params.isExpandMdm());
|
||||
|
||||
int submissionCount = 0;
|
||||
try {
|
||||
for (String resourceType : params.getResourceTypes()) {
|
||||
providerParams.setResourceType(resourceType);
|
||||
|
||||
// filters are the filters for searching
|
||||
Iterator<ResourcePersistentId> pidIterator = myBulkExportProcessor.getResourcePidIterator(providerParams);
|
||||
List<Id> idsToSubmit = new ArrayList<>();
|
||||
while (pidIterator.hasNext()) {
|
||||
ResourcePersistentId pid = pidIterator.next();
|
||||
|
||||
idsToSubmit.add(Id.getIdFromPID(pid, resourceType));
|
||||
|
||||
// >= so that we know (with confidence)
|
||||
// that every batch is <= 1000 items
|
||||
if (idsToSubmit.size() >= MAX_IDS_TO_BATCH) {
|
||||
submitWorkChunk(idsToSubmit, resourceType, params, theDataSink);
|
||||
submissionCount++;
|
||||
idsToSubmit = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// if we have any other Ids left, submit them now
|
||||
if (!idsToSubmit.isEmpty()) {
|
||||
submitWorkChunk(idsToSubmit, resourceType, params, theDataSink);
|
||||
submissionCount++;
|
||||
idsToSubmit = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ourLog.error(ex.getMessage());
|
||||
|
||||
theDataSink.recoveredError(ex.getMessage());
|
||||
|
||||
throw new JobExecutionFailedException(Msg.code(2104) + " : " + ex.getMessage());
|
||||
}
|
||||
|
||||
ourLog.info("Submitted {} groups of ids for processing", submissionCount);
|
||||
return RunOutcome.SUCCESS;
|
||||
}
|
||||
|
||||
private void submitWorkChunk(List<Id> theIds,
|
||||
String theResourceType,
|
||||
BulkExportJobParameters theParams,
|
||||
IJobDataSink<BulkExportIdList> theDataSink) {
|
||||
BulkExportIdList idList = new BulkExportIdList();
|
||||
|
||||
idList.setIds(theIds);
|
||||
|
||||
idList.setResourceType(theResourceType);
|
||||
|
||||
theDataSink.accept(idList);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
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.jobs.export.models.BulkExportBinaryFileId;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportExpandedResources;
|
||||
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.util.BinaryUtil;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
public class WriteBinaryStep implements IJobStepWorker<BulkExportJobParameters, BulkExportExpandedResources, BulkExportBinaryFileId> {
|
||||
private static final Logger ourLog = getLogger(WriteBinaryStep.class);
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public RunOutcome run(@Nonnull StepExecutionDetails<BulkExportJobParameters, BulkExportExpandedResources> theStepExecutionDetails,
|
||||
@Nonnull IJobDataSink<BulkExportBinaryFileId> theDataSink) throws JobExecutionFailedException {
|
||||
|
||||
BulkExportExpandedResources expandedResources = theStepExecutionDetails.getData();
|
||||
|
||||
ourLog.info("Write binary step of Job Export");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
IFhirResourceDao<IBaseBinary> binaryDao = myDaoRegistry.getResourceDao("Binary");
|
||||
|
||||
IBaseBinary binary = BinaryUtil.newBinary(myFhirContext);
|
||||
|
||||
// TODO
|
||||
// should be dependent on the
|
||||
// output format in parameters
|
||||
// but for now, only NDJSON is supported
|
||||
binary.setContentType(Constants.CT_FHIR_NDJSON);
|
||||
|
||||
int processedRecordsCount = 0;
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
try (OutputStreamWriter streamWriter = getStreamWriter(outputStream)) {
|
||||
for (String stringified : expandedResources.getStringifiedResources()) {
|
||||
streamWriter.append(stringified);
|
||||
streamWriter.append("\n");
|
||||
processedRecordsCount++;
|
||||
}
|
||||
streamWriter.flush();
|
||||
outputStream.flush();
|
||||
}
|
||||
binary.setContent(outputStream.toByteArray());
|
||||
} catch (IOException ex) {
|
||||
String errorMsg = String.format("Failure to process resource of type %s : %s",
|
||||
expandedResources.getResourceType(),
|
||||
ex.getMessage());
|
||||
ourLog.error(errorMsg);
|
||||
|
||||
throw new JobExecutionFailedException(Msg.code(2105) + errorMsg);
|
||||
}
|
||||
|
||||
DaoMethodOutcome outcome = binaryDao.create(binary,
|
||||
new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
IIdType id = outcome.getId();
|
||||
|
||||
BulkExportBinaryFileId bulkExportBinaryFileId = new BulkExportBinaryFileId();
|
||||
bulkExportBinaryFileId.setBinaryId(id.getValueAsString());
|
||||
bulkExportBinaryFileId.setResourceType(expandedResources.getResourceType());
|
||||
theDataSink.accept(bulkExportBinaryFileId);
|
||||
|
||||
ourLog.info("Binary writing complete for {} resources of type {}.",
|
||||
processedRecordsCount,
|
||||
expandedResources.getResourceType());
|
||||
|
||||
return RunOutcome.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an output stream writer
|
||||
* (exposed for testing)
|
||||
*/
|
||||
protected OutputStreamWriter getStreamWriter(ByteArrayOutputStream theOutputStream) {
|
||||
return new OutputStreamWriter(theOutputStream, Constants.CHARSET_UTF8);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class BulkExportBinaryFileId extends BulkExportJobBase {
|
||||
|
||||
@JsonProperty("binaryId")
|
||||
private String myBinaryId;
|
||||
|
||||
@JsonProperty("resourceType")
|
||||
private String myResourceType;
|
||||
|
||||
public BulkExportBinaryFileId() {
|
||||
}
|
||||
|
||||
public String getBinaryId() {
|
||||
return myBinaryId;
|
||||
}
|
||||
|
||||
public void setBinaryId(String theBinaryId) {
|
||||
myBinaryId = theBinaryId;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String theResourceType) {
|
||||
myResourceType = theResourceType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BulkExportExpandedResources extends BulkExportJobBase {
|
||||
|
||||
/**
|
||||
* List of stringified resources ready for writing
|
||||
* to a file/binary.
|
||||
*/
|
||||
@JsonProperty("resources")
|
||||
private List<String> myStringifiedResources;
|
||||
|
||||
/**
|
||||
* only a single resource type per batch step
|
||||
*/
|
||||
@JsonProperty("resourceType")
|
||||
private String myResourceType;
|
||||
|
||||
public List<String> getStringifiedResources() {
|
||||
return myStringifiedResources;
|
||||
}
|
||||
|
||||
public void setStringifiedResources(List<String> theStringifiedResources) {
|
||||
myStringifiedResources = theStringifiedResources;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String theResourceType) {
|
||||
myResourceType = theResourceType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package ca.uhn.fhir.batch2.jobs.export.models;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* hapi-fhir-storage-batch2-jobs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.jobs.models.Id;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BulkExportIdList extends BulkExportJobBase {
|
||||
|
||||
/**
|
||||
* List of Id objects for serialization
|
||||
*/
|
||||
@JsonProperty("ids")
|
||||
private List<Id> myIds;
|
||||
|
||||
@JsonProperty("resourceType")
|
||||
private String myResourceType;
|
||||
|
||||
public List<Id> getIds() {
|
||||
return myIds;
|
||||
}
|
||||
|
||||
public void setIds(List<Id> theIds) {
|
||||
myIds = theIds;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String theResourceType) {
|
||||
myResourceType = theResourceType;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue