Merge pull request #1984 from jamesagnew/batch-empi-job

This commit is contained in:
Tadgh 2020-08-04 13:21:25 -07:00 committed by GitHub
commit 750d693fc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1589 additions and 48 deletions

View File

@ -0,0 +1,5 @@
---
type: add
issue: 1984
title: "Two new operations have been added for EMPI: `$empi-clear` and `$empi-submit`. `$empi-clear` will delete EMPI links,
and related Person objects. `$empi-submit` will submit all matching resources for EMPI processing."

View File

@ -20,3 +20,4 @@
The method `FhirContext#getResourceNames()` has been renamed to `FhirContext#getResourceTypes()`. HAPI currently
goes back and forth between the two, but is consolidating on `Types`.
"

View File

@ -447,3 +447,141 @@ This might result in a response such as the following:
]
}
```
## Clearing EMPI Links
The `$empi-clear` operation is used to batch-delete EMPI links and related persons from the database. This operation is meant to
be used during the rules-tuning phase of the EMPI implementation so that you can quickly test your ruleset.
It permits the user to reset the state of their EMPI system without manual deletion of all related links and Persons.
After the operation is complete, all targeted EMPI links are removed from the system, and their related Person resources are deleted and expunged
from the server.
This operation takes a single optional Parameter.
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Cardinality</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>resourceType</td>
<td>String</td>
<td>0..1</td>
<td>
The target Resource type you would like to clear. Currently limited to Patient/Practitioner. If omitted, will operate over all links.
</td>
</tr>
</tbody>
</table>
### Example
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-clear
```
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "resourceType",
"valueString": "Patient"
} ]
}
```
This operation returns the number of EMPI links that were cleared. The following is a sample response:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "reset",
"valueDecimal": 5
} ]
}
```
## Batch-creating EMPI Links
Call the `$empi-submit` operation to submit patients and practitioners for EMPI processing. In the rules-tuning phase of your setup, you can use `$empi-submit` to apply EMPI rules across multiple Resources.
An important thing to note is that this operation only submits the resources for processing. Actual EMPI processing is run asynchronously, and depending on the size
of the affected bundle of resources, may take some time to complete.
After the operation is complete, all resources that matched the criteria will now have at least one EMPI link attached to them.
This operation takes a single optional criteria parameter unless it is called on a specific instance.
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Cardinality</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>criteria</td>
<td>String</td>
<td>0..1</td>
<td>
The search critiera used to filter resources.
</td>
</tr>
</tbody>
</table>
### Example
This operation can be executed at the Server level, Resource level, or Instance level.
Use an HTTP POST to the following URL to invoke this operation with matching criteria:
```url
http://example.com/$empi-submit
http://example.com/Patient/$empi-submit
http://example.com/Practitioner/$empi-submit
```
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"criteria": "",
"valueString": "birthDate=2020-07-28"
} ]
}
```
This operation returns the number of resources that were submitted for EMPI processing. The following is a sample response:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "submitted",
"valueDecimal": 5
} ]
}
```
This operation can also be done at the Instance level. When this is the case, the operations accepts no parameters.
The following are examples of Instance level POSTs, which require no parameters.
```url
http://example.com/Patient/123/$empi-submit
http://example.com/Practitioner/456/$empi-submit
```

View File

@ -127,6 +127,10 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
return myList.size();
}
public void removeAll() {
this.removeIf(x -> true);
}
@Override
public String toString() {
return myList.toString();

View File

@ -78,13 +78,11 @@ public class BulkExportJobConfig {
return new CreateBulkExportEntityTasklet();
}
@Bean
public JobParametersValidator bulkJobParameterValidator() {
return new BulkExportJobParameterValidator();
}
@Bean
public Step bulkExportGenerateResourceFilesStep() {
return myStepBuilderFactory.get("bulkExportGenerateResourceFilesStep")
@ -95,8 +93,6 @@ public class BulkExportJobConfig {
.build();
}
@Bean
@JobScope
public BulkExportJobCloser bulkExportJobCloser() {

View File

@ -35,7 +35,6 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.BaseTag;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
@ -136,8 +135,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Autowired
protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
@Autowired
protected DaoConfig myDaoConfig;
@Autowired
@ -148,16 +145,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private SearchBuilderFactory mySearchBuilderFactory;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperService;
@Autowired
private HapiTransactionService myTransactionService;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
private IInstanceValidatorModule myInstanceValidator;
private String myResourceName;
private Class<T> myResourceType;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperService;
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
private HapiTransactionService myTransactionService;
@Override
@Transactional
@ -707,6 +704,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return myResourceName;
}
@Override
public Class<T> getResourceType() {
return myResourceType;

View File

@ -23,12 +23,15 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import java.io.Closeable;
import java.util.Collection;
import java.util.Iterator;
public interface IResultIterator extends Iterator<ResourcePersistentId>, Closeable {
int getSkippedCount();
int getNonSkippedCount();
int getNonSkippedCount();
Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize);
}

View File

@ -1306,12 +1306,22 @@ public class SearchBuilder implements ISearchBuilder {
return myNonSkipCount;
}
@Override
public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
Collection<ResourcePersistentId> batch = new ArrayList<>();
while (this.hasNext() && batch.size() < theBatchSize) {
batch.add(this.next());
}
return batch;
}
@Override
public void close() {
if (myResultsIterator != null) {
myResultsIterator.close();
}
}
}
private static class CountQueryIterator implements Iterator<Long> {

View File

@ -33,9 +33,9 @@ public interface IResourceExpungeService {
void expungeHistoricalVersions(RequestDetails theRequestDetails, List<Long> thePartition, AtomicInteger theRemainingCount);
void expungeCurrentVersionOfResources(RequestDetails theRequestDetails, List<Long> thePartition, AtomicInteger theRemainingCount);
void expungeCurrentVersionOfResources(RequestDetails theRequestDetails, List<Long> theResourceIds, AtomicInteger theRemainingCount);
void expungeHistoricalVersionsOfIds(RequestDetails theRequestDetails, List<Long> thePartition, AtomicInteger theRemainingCount);
void expungeHistoricalVersionsOfIds(RequestDetails theRequestDetails, List<Long> theResourceIds, AtomicInteger theRemainingCount);
void deleteAllSearchParams(Long theResourceId);
}

View File

@ -67,7 +67,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Service
class ResourceExpungeService implements IResourceExpungeService {
public class ResourceExpungeService implements IResourceExpungeService {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceExpungeService.class);
@Autowired

View File

@ -37,13 +37,10 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -55,7 +52,6 @@ import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -66,7 +62,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -483,7 +478,7 @@ public class IdHelperService {
}
public Map<Long, IIdType> getPidToIdMap(Collection<IIdType> theIds, RequestDetails theRequestDetails) {
return theIds.stream().collect(Collectors.toMap(t->getPidOrThrowException(t), Function.identity()));
return theIds.stream().collect(Collectors.toMap(this::getPidOrThrowException, Function.identity()));
}
public IIdType resourceIdFromPidOrThrowException(Long thePid) {

View File

@ -51,6 +51,8 @@ public class EmpiLink {
public static final int VERSION_LENGTH = 16;
private static final int MATCH_RESULT_LENGTH = 16;
private static final int LINK_SOURCE_LENGTH = 16;
public static final int TARGET_TYPE_LENGTH = 40;
@SequenceGenerator(name = "SEQ_EMPI_LINK_ID", sequenceName = "SEQ_EMPI_LINK_ID")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_EMPI_LINK_ID")
@ -111,6 +113,9 @@ public class EmpiLink {
myVersion = theVersion;
}
@Column(name = "TARGET_TYPE", nullable = true, length = TARGET_TYPE_LENGTH)
private String myEmpiTargetType;
public Long getId() {
return myId;
}
@ -276,6 +281,7 @@ public class EmpiLink {
return new ToStringBuilder(this)
.append("myPersonPid", myPersonPid)
.append("myTargetPid", myTargetPid)
.append("myEmpiTargetType", myEmpiTargetType)
.append("myMatchResult", myMatchResult)
.append("myLinkSource", myLinkSource)
.append("myEidMatch", myEidMatch)
@ -283,4 +289,12 @@ public class EmpiLink {
.append("myScore", myScore)
.toString();
}
public String getEmpiTargetType() {
return myEmpiTargetType;
}
public void setEmpiTargetType(String theEmpiTargetType) {
myEmpiTargetType = theEmpiTargetType;
}
}

View File

@ -44,7 +44,6 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@ -77,6 +76,7 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
return createExpungeResponse(outcome);
}
public IFhirResourceDao<T> getDao() {
return myDao;
}

View File

@ -112,6 +112,8 @@ public class JpaResourceProviderR4<T extends IAnyResource> extends BaseJpaResour
return super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest);
}
@Operation(name = OPERATION_META, idempotent = true, returnParameters = {
@OperationParam(name = "return", type = Meta.class)
})

View File

@ -221,6 +221,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
return template.execute(theStatus -> toResourceList(sb, pidsSubList));
}
/**
* Returns false if the entity can't be found
*/

View File

@ -13,7 +13,6 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
@ -24,12 +23,11 @@ import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -668,6 +666,15 @@ public class SearchCoordinatorSvcImplTest {
return myCount;
}
@Override
public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
Collection<ResourcePersistentId> batch = new ArrayList<>();
while (this.hasNext() && batch.size() < theBatchSize) {
batch.add(this.next());
}
return batch;
}
@Override
public void close() {
// nothing
@ -704,6 +711,15 @@ public class SearchCoordinatorSvcImplTest {
return myCount;
}
@Override
public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
Collection<ResourcePersistentId> batch = new ArrayList<>();
while (this.hasNext() && batch.size() < theBatchSize) {
batch.add(this.next());
}
return batch;
}
@Override
public void close() {
// nothing
@ -775,6 +791,15 @@ public class SearchCoordinatorSvcImplTest {
return 0;
}
@Override
public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
Collection<ResourcePersistentId> batch = new ArrayList<>();
while (this.hasNext() && batch.size() < theBatchSize) {
batch.add(this.next());
}
return batch;
}
@Override
public void close() {
// nothing

View File

@ -79,6 +79,7 @@ public class EmpiMessageHandler implements MessageHandler {
handleCreatePatientOrPractitioner(theMsg, empiContext);
break;
case UPDATE:
case MANUALLY_TRIGGERED:
handleUpdatePatientOrPractitioner(theMsg, empiContext);
break;
case DELETE:
@ -106,6 +107,9 @@ public class EmpiMessageHandler implements MessageHandler {
case UPDATE:
empiOperation = EmpiTransactionContext.OperationType.UPDATE;
break;
case MANUALLY_TRIGGERED:
empiOperation = EmpiTransactionContext.OperationType.BATCH;
break;
case DELETE:
default:
ourLog.trace("Not creating an EmpiTransactionContext for {}", theMsg.getOperationType());

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
@ -46,7 +47,9 @@ import ca.uhn.fhir.jpa.empi.svc.EmpiLinkSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkUpdaterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchFinderSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResetSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
@ -159,6 +162,11 @@ public class EmpiConsumerConfig {
return new EmpiLinkQuerySvcImpl();
}
@Bean
IEmpiResetSvc empiResetSvc(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl ) {
return new EmpiResetSvcImpl(theEmpiLinkDaoSvc, theEmpiPersonDeletingSvcImpl);
}
@Bean
EmpiCandidateSearchSvc empiCandidateSearchSvc() {
return new EmpiCandidateSearchSvc();
@ -190,7 +198,7 @@ public class EmpiConsumerConfig {
}
@Bean
IEmpiLinkUpdaterSvc manualLinkUpdaterSvc() {
IEmpiLinkUpdaterSvc manualLinkUpdaterSvc() {
return new EmpiLinkUpdaterSvcImpl();
}

View File

@ -21,12 +21,19 @@ package ca.uhn.fhir.jpa.empi.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader;
import ca.uhn.fhir.jpa.empi.svc.EmpiBatchSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiChannelSubmitterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class EmpiSubmitterConfig {
@ -50,4 +57,20 @@ public class EmpiSubmitterConfig {
EmpiLinkDeleteSvc empiLinkDeleteSvc() {
return new EmpiLinkDeleteSvc();
}
@Bean
EmpiPersonDeletingSvc empiPersonDeletingSvc() {
return new EmpiPersonDeletingSvc();
}
@Bean
@Lazy
IEmpiChannelSubmitterSvc empiChannelSubmitterSvc(FhirContext theFhirContext, IChannelFactory theChannelFactory) {
return new EmpiChannelSubmitterSvcImpl(theFhirContext, theChannelFactory);
}
@Bean
IEmpiBatchSvc empiBatchService() {
return new EmpiBatchSvcImpl();
}
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.empi.dao;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
@ -29,7 +30,6 @@ import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
@ -42,6 +42,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class EmpiLinkDaoSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@ -52,6 +53,8 @@ public class EmpiLinkDaoSvc {
private EmpiLinkFactory myEmpiLinkFactory;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private FhirContext myFhirContext;
@Transactional
public EmpiLink createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theTarget, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, @Nullable EmpiTransactionContext theEmpiTransactionContext) {
@ -64,6 +67,7 @@ public class EmpiLinkDaoSvc {
// Preserve these flags for link updates
empiLink.setEidMatch(theMatchOutcome.isEidMatch() | empiLink.isEidMatch());
empiLink.setNewPerson(theMatchOutcome.isNewPerson() | empiLink.isNewPerson());
empiLink.setEmpiTargetType(myFhirContext.getResourceType(theTarget));
if (empiLink.getScore() != null) {
empiLink.setScore(Math.max(theMatchOutcome.score, empiLink.getScore()));
} else {
@ -77,7 +81,6 @@ public class EmpiLinkDaoSvc {
return empiLink;
}
@Nonnull
public EmpiLink getOrCreateEmpiLinkByPersonPidAndTargetPid(Long thePersonPid, Long theResourcePid) {
Optional<EmpiLink> oExisting = getLinkByPersonPidAndTargetPid(thePersonPid, theResourcePid);
@ -103,6 +106,14 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findOne(example);
}
/**
* Given a Target Pid, and a match result, return all links that match these criteria.
*
* @param theTargetPid the target of the relationship.
* @param theMatchResult the Match Result of the relationship
*
* @return a list of {@link EmpiLink} entities matching these criteria.
*/
public List<EmpiLink> getEmpiLinksByTargetPidAndMatchResult(Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
@ -111,6 +122,13 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findAll(example);
}
/**
* Given a target Pid, return its Matched EmpiLink. There can only ever be at most one of these, but its possible
* the target has no matches, and may return an empty optional.
*
* @param theTargetPid The Pid of the target you wish to find the matching link for.
* @return the {@link EmpiLink} that contains the Match information for the target.
*/
public Optional<EmpiLink> getMatchedLinkForTargetPid(Long theTargetPid) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
@ -119,6 +137,13 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findOne(example);
}
/**
* Given an IBaseResource, return its Matched EmpiLink. There can only ever be at most one of these, but its possible
* the target has no matches, and may return an empty optional.
*
* @param theTarget The IBaseResource representing the target you wish to find the matching link for.
* @return the {@link EmpiLink} that contains the Match information for the target.
*/
public Optional<EmpiLink> getMatchedLinkForTarget(IBaseResource theTarget) {
Long pid = myIdHelperService.getPidOrNull(theTarget);
if (pid == null) {
@ -132,6 +157,15 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findOne(example);
}
/**
* Given a person a target and a match result, return the matching EmpiLink, if it exists.
*
* @param thePersonPid The Pid of the Person in the relationship
* @param theTargetPid The Pid of the target in the relationship
* @param theMatchResult The MatchResult you are looking for.
*
* @return an Optional {@link EmpiLink} containing the matched link if it exists.
*/
public Optional<EmpiLink> getEmpiLinksByPersonPidTargetPidAndMatchResult(Long thePersonPid, Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setPersonPid(thePersonPid);
@ -163,12 +197,25 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findOne(example);
}
/**
* Delete a given EmpiLink. Note that this does not clear out the Person, or the Person's related links.
* It is a simple entity delete.
*
* @param theEmpiLink the EmpiLink to delete.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteLink(EmpiLink theEmpiLink) {
myEmpiLinkDao.delete(theEmpiLink);
}
public List<EmpiLink> findEmpiLinksByPersonId(IBaseResource thePersonResource) {
/**
* Given a Person, return all links in which they are the source Person of the {@link EmpiLink}
*
* @param thePersonResource The {@link IBaseResource} Person who's links you would like to retrieve.
*
* @return A list of all {@link EmpiLink} entities in which thePersonResource is the source Person.
*/
public List<EmpiLink> findEmpiLinksByPerson(IBaseResource thePersonResource) {
Long pid = myIdHelperService.getPidOrNull(thePersonResource);
if (pid == null) {
return Collections.emptyList();
@ -178,6 +225,46 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findAll(example);
}
/**
* Delete all {@link EmpiLink} entities, and return all resource PIDs from the source of the relationship.
*
* @return A list of Long representing the related Person Pids.
*/
@Transactional
public List<Long> deleteAllEmpiLinksAndReturnPersonPids() {
List<EmpiLink> all = myEmpiLinkDao.findAll();
return deleteEmpiLinksAndReturnPersonPids(all);
}
private List<Long> deleteEmpiLinksAndReturnPersonPids(List<EmpiLink> theLinks) {
List<Long> collect = theLinks.stream().map(EmpiLink::getPersonPid).distinct().collect(Collectors.toList());
myEmpiLinkDao.deleteAll(theLinks);
return collect;
}
/**
* Given a valid {@link String}, delete all {@link EmpiLink} entities for that type, and get the Pids
* for the Person resources which were the sources of the links.
*
* @param theTargetType the type of relationship you would like to delete.
*
* @return A list of longs representing the Pids of the Person resources used as the sources of the relationships that were deleted.
*/
public List<Long> deleteAllEmpiLinksOfTypeAndReturnPersonPids(String theTargetType) {
EmpiLink link = new EmpiLink();
link.setEmpiTargetType(theTargetType);
Example<EmpiLink> exampleLink = Example.of(link);
List<EmpiLink> allOfType = myEmpiLinkDao.findAll(exampleLink);
return deleteEmpiLinksAndReturnPersonPids(allOfType);
}
/**
* Persist an EmpiLink to the database.
*
* @param theEmpiLink the link to save.
*
* @return the persisted {@link EmpiLink} entity.
*/
public EmpiLink save(EmpiLink theEmpiLink) {
if (theEmpiLink.getCreated() == null) {
theEmpiLink.setCreated(new Date());
@ -186,11 +273,27 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.save(theEmpiLink);
}
/**
* Given an example {@link EmpiLink}, return all links from the database which match the example.
*
* @param theExampleLink The EmpiLink containing the data we would like to search for.
*
* @return a list of {@link EmpiLink} entities which match the example.
*/
public List<EmpiLink> findEmpiLinkByExample(Example<EmpiLink> theExampleLink) {
return myEmpiLinkDao.findAll(theExampleLink);
}
public List<EmpiLink> findEmpiLinksByTarget(Patient theTargetResource) {
/**
* Given a target {@link IBaseResource}, return all {@link EmpiLink} entities in which this target is the target
* of the relationship. This will show you all links for a given Patient/Practitioner.
*
* @param theTargetResource the target resource to find links for.
*
* @return all links for the target.
*/
public List<EmpiLink> findEmpiLinksByTarget(IBaseResource theTargetResource) {
Long pid = myIdHelperService.getPidOrNull(theTargetResource);
if (pid == null) {
return Collections.emptyList();
@ -200,7 +303,13 @@ public class EmpiLinkDaoSvc {
return myEmpiLinkDao.findAll(example);
}
/**
* Factory delegation method, whenever you need a new EmpiLink, use this factory method.
* //TODO Should we make the constructor private for EmpiLink? or work out some way to ensure they can only be instantiated via factory.
* @return A new {@link EmpiLink}.
*/
public EmpiLink newEmpiLink() {
return myEmpiLinkFactory.newEmpiLink();
}
}

View File

@ -32,6 +32,11 @@ public class EmpiLinkFactory {
myEmpiSettings = theEmpiSettings;
}
/**
* Create a new EmpiLink, populating it with the version of the ruleset used to create it.
*
* @return the new {@link EmpiLink}
*/
public EmpiLink newEmpiLink() {
return new EmpiLink(myEmpiSettings.getRuleVersion());
}

View File

@ -60,6 +60,7 @@ public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
@Autowired
private PersonHelper myPersonHelper;
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void blockManualPersonManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {

View File

@ -0,0 +1,140 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.util.EmpiUtil;
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.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.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private EmpiSearchParamSvc myEmpiSearchParamSvc;
@Autowired
private IEmpiChannelSubmitterSvc myEmpiChannelSubmitterSvc;
private static final int BUFFER_SIZE = 100;
@Override
@Transactional
public long runEmpiOnAllTargetTypes(String theCriteria) {
long submittedCount = 0;
submittedCount += runEmpiOnPatientType(theCriteria);
submittedCount += runEmpiOnPractitionerType(theCriteria);
return submittedCount;
}
@Override
@Transactional
public long runEmpiOnTargetType(String theTargetType, String theCriteria) {
resolveTargetTypeOrThrowException(theTargetType);
SearchParameterMap spMap = myEmpiSearchParamSvc.getSearchParameterMapFromCriteria(theTargetType, theCriteria);
spMap.setLoadSynchronousUpTo(BUFFER_SIZE);
ISearchBuilder searchBuilder = myEmpiSearchParamSvc.generateSearchBuilderForType(theTargetType);
return submitAllMatchingResourcesToEmpiChannel(spMap, searchBuilder);
}
private long submitAllMatchingResourcesToEmpiChannel(SearchParameterMap theSpMap, ISearchBuilder theSearchBuilder) {
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(null, UUID.randomUUID().toString());
long total = 0;
try (IResultIterator query = theSearchBuilder.createQuery(theSpMap, searchRuntimeDetails, null, RequestPartitionId.defaultPartition())) {
Collection<ResourcePersistentId> pidBatch;
do {
pidBatch = query.getNextResultBatch(BUFFER_SIZE);
total += loadPidsAndSubmitToEmpiChannel(theSearchBuilder, pidBatch);
} while (query.hasNext());
} catch (IOException theE) {
throw new InternalErrorException("Failure while attempting to query resources for " + ProviderConstants.OPERATION_EMPI_BATCH_RUN, theE);
}
return total;
}
/**
* Given a collection of ResourcePersistentId objects, and a search builder, load the IBaseResources and submit them to
* the EMPI channel for processing.
*
* @param theSearchBuilder the related DAO search builder.
* @param thePidsToSubmit The collection of PIDs whos resources you want to submit for EMPI processing.
*
* @return The total count of submitted resources.
*/
private long loadPidsAndSubmitToEmpiChannel(ISearchBuilder theSearchBuilder, Collection<ResourcePersistentId> thePidsToSubmit) {
List<IBaseResource> resourcesToSubmit = new ArrayList<>();
theSearchBuilder.loadResourcesByPid(thePidsToSubmit, Collections.emptyList(), resourcesToSubmit, false, null);
resourcesToSubmit
.forEach(resource -> myEmpiChannelSubmitterSvc.submitResourceToEmpiChannel(resource));
return resourcesToSubmit.size();
}
@Override
@Transactional
public long runEmpiOnPractitionerType(String theCriteria) {
return runEmpiOnTargetType("Practitioner", theCriteria);
}
@Override
@Transactional
public long runEmpiOnPatientType(String theCriteria) {
return runEmpiOnTargetType("Patient", theCriteria);
}
@Override
@Transactional
public long runEmpiOnTarget(IIdType theId) {
resolveTargetTypeOrThrowException(theId.getResourceType());
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theId.getResourceType());
IBaseResource read = resourceDao.read(theId);
myEmpiChannelSubmitterSvc.submitResourceToEmpiChannel(read);
return 1;
}
private void resolveTargetTypeOrThrowException(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.OPERATION_EMPI_BATCH_RUN + " does not support resource type: " + theResourceType);
}
}
}

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageChannel;
import static ca.uhn.fhir.empi.api.IEmpiSettings.EMPI_CHANNEL_NAME;
/**
* This class is responsible for manual submissions of {@link IAnyResource} resources onto the Empi Channel.
*/
public class EmpiChannelSubmitterSvcImpl implements IEmpiChannelSubmitterSvc {
private MessageChannel myEmpiChannelProducer;
private FhirContext myFhirContext;
private IChannelFactory myChannelFactory;
@Override
public void submitResourceToEmpiChannel(IBaseResource theResource) {
ResourceModifiedJsonMessage resourceModifiedJsonMessage = new ResourceModifiedJsonMessage();
ResourceModifiedMessage resourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, theResource, ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedMessage.setOperationType(ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedJsonMessage.setPayload(resourceModifiedMessage);
getEmpiChannelProducer().send(resourceModifiedJsonMessage);
}
@Autowired
public EmpiChannelSubmitterSvcImpl(FhirContext theFhirContext, IChannelFactory theIChannelFactory) {
myFhirContext = theFhirContext;
myChannelFactory = theIChannelFactory;
}
private void init() {
ChannelProducerSettings channelSettings = new ChannelProducerSettings();
myEmpiChannelProducer= myChannelFactory.getOrCreateProducer(EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, channelSettings);
}
private MessageChannel getEmpiChannelProducer() {
if (myEmpiChannelProducer == null) {
init();
}
return myEmpiChannelProducer;
}
}

View File

@ -107,7 +107,7 @@ public class EmpiLinkSvcImpl implements IEmpiLinkSvc {
public void syncEmpiLinksToPersonLinks(IAnyResource thePersonResource, EmpiTransactionContext theEmpiTransactionContext) {
int origLinkCount = myPersonHelper.getLinkCount(thePersonResource);
List<EmpiLink> empiLinks = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(thePersonResource);
List<EmpiLink> empiLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(thePersonResource);
List<IBaseBackboneElement> newLinks = empiLinks.stream()
.filter(link -> link.isMatch() || link.isPossibleMatch())

View File

@ -0,0 +1,104 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.slf4j.LoggerFactory.getLogger;
@Service
public class EmpiPersonDeletingSvc {
private static final Logger ourLog = getLogger(EmpiPersonDeletingSvc.class);
/**
* This is here for the case of possible infinite loops. Technically batch conflict deletion should handle this, but this is an escape hatch.
*/
private static final int MAXIMUM_DELETE_ATTEMPTS = 100000;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private ExpungeService myExpungeService;
/**
* Function which will delete all resources by their PIDs, and also delete any resources that were undeletable due to
* VersionConflictException
*
* @param theLongs
*/
@Transactional
public void deleteResourcesAndHandleConflicts(List<Long> theLongs) {
DeleteConflictList
deleteConflictList = new DeleteConflictList();
theLongs.stream().forEach(pid -> deleteCascade(pid, deleteConflictList));
IFhirResourceDao personDao = myDaoRegistry.getResourceDao("Person");
int batchCount = 0;
while (!deleteConflictList.isEmpty()) {
deleteConflictBatch(deleteConflictList, personDao);
batchCount += 1;
if (batchCount > MAXIMUM_DELETE_ATTEMPTS) {
throw new IllegalStateException("Person deletion seems to have entered an infinite loop. Aborting");
}
}
}
/**
* Use the expunge service to expunge all historical and current versions of the resources associated to the PIDs.
*/
public void expungeHistoricalAndCurrentVersionsOfIds(List<Long> theLongs) {
ExpungeOptions options = new ExpungeOptions();
options.setExpungeDeletedResources(true);
options.setExpungeOldVersions(true);
theLongs
.forEach(personId -> myExpungeService.expunge("Person", personId, null, options, null));
}
private void deleteCascade(Long pid, DeleteConflictList theDeleteConflictList) {
ourLog.debug("About to cascade delete: {}", pid);
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao("Person");
resourceDao.delete(new IdType("Person/" + pid), theDeleteConflictList, null, null);
}
private void deleteConflictBatch(DeleteConflictList theDcl, IFhirResourceDao<IBaseResource> theDao) {
DeleteConflictList newBatch = new DeleteConflictList();
for (DeleteConflict next : theDcl) {
IdDt nextSource = next.getSourceId();
ourLog.info("Have delete conflict {} - Cascading delete", nextSource);
theDao.delete(nextSource.toVersionless(), newBatch, null, null);
}
theDcl.removeAll();
theDcl.addAll(newBatch);
}
}

View File

@ -90,8 +90,8 @@ public class EmpiPersonMergerSvcImpl implements IEmpiPersonMergerSvc {
}
private void mergeLinks(IAnyResource theFromPerson, IAnyResource theToPerson, Long theToPersonPid, EmpiTransactionContext theEmpiTransactionContext) {
List<EmpiLink> incomingLinks = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(theFromPerson);
List<EmpiLink> origLinks = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(theToPerson);
List<EmpiLink> incomingLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theFromPerson);
List<EmpiLink> origLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theToPerson);
// For each incomingLink, either ignore it, move it, or replace the original one

View File

@ -0,0 +1,73 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* This class is responsible for clearing out existing EMPI links, as well as deleting all persons related to those EMPI Links.
*
*/
public class EmpiResetSvcImpl implements IEmpiResetSvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiResetSvcImpl.class);
final EmpiLinkDaoSvc myEmpiLinkDaoSvc;
final EmpiPersonDeletingSvc myEmpiPersonDeletingSvcImpl;
@Autowired
public EmpiResetSvcImpl(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl) {
myEmpiLinkDaoSvc = theEmpiLinkDaoSvc;
myEmpiPersonDeletingSvcImpl = theEmpiPersonDeletingSvcImpl;
}
@Override
public long expungeAllEmpiLinksOfTargetType(String theResourceType) {
throwExceptionIfInvalidTargetType(theResourceType);
List<Long> longs = myEmpiLinkDaoSvc.deleteAllEmpiLinksOfTypeAndReturnPersonPids(theResourceType);
myEmpiPersonDeletingSvcImpl.deleteResourcesAndHandleConflicts(longs);
myEmpiPersonDeletingSvcImpl.expungeHistoricalAndCurrentVersionsOfIds(longs);
return longs.size();
}
private void throwExceptionIfInvalidTargetType(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.EMPI_CLEAR + " does not support resource type: " + theResourceType);
}
}
@Override
public long removeAllEmpiLinks() {
List<Long> personPids = myEmpiLinkDaoSvc.deleteAllEmpiLinksAndReturnPersonPids();
myEmpiPersonDeletingSvcImpl.deleteResourcesAndHandleConflicts(personPids);
myEmpiPersonDeletingSvcImpl.expungeHistoricalAndCurrentVersionsOfIds(personPids);
return personPids.size();
}
}

View File

@ -23,11 +23,16 @@ package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.context.FhirContext;
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.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -44,6 +49,10 @@ public class EmpiSearchParamSvc implements ISearchParamRetriever {
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private SearchParamExtractorService mySearchParamExtractorService;
@Autowired
private SearchBuilderFactory mySearchBuilderFactory;
@Autowired
private DaoRegistry myDaoRegistry;
public SearchParameterMap mapFromCriteria(String theResourceType, String theResourceCriteria) {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType);
@ -60,4 +69,28 @@ public class EmpiSearchParamSvc implements ISearchParamRetriever {
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
return mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
}
/**
* Given a target type, and a criteria string of the shape name=x&birthDate=y, generate a {@link SearchParameterMap}
* that represents this query.
*
* @param theTargetType the resource type to execute the search on
* @param theCriteria the string search criteria.
*
* @return the generated SearchParameterMap, or an empty one if there is no criteria.
*/
public SearchParameterMap getSearchParameterMapFromCriteria(String theTargetType, String theCriteria) {
SearchParameterMap spMap;
if (StringUtils.isBlank(theCriteria)) {
spMap = new SearchParameterMap();
} else {
spMap = mapFromCriteria(theTargetType, theCriteria);
}
return spMap;
}
public ISearchBuilder generateSearchBuilderForType(String theTargetType) {
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theTargetType);
return mySearchBuilderFactory.newSearchBuilder(resourceDao, theTargetType, resourceDao.getResourceType());
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
@ -74,6 +75,8 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
private static final ContactPoint TEST_TELECOM = new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue("555-555-5555");
private static final String NAME_GIVEN_FRANK = "Frank";
protected static final String FRANK_ID = "ID.FRANK.789";
@Autowired
protected FhirContext myFhirContext;
@ -101,6 +104,8 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
@Autowired
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
private IEmpiBatchSvc myEmpiBatchService;
protected ServletRequestDetails myRequestDetails = new ServletRequestDetails(null);
@ -157,6 +162,14 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
patient.setId(outcome.getId());
return patient;
}
@Nonnull
protected Practitioner createPractitioner(Practitioner thePractitioner) {
//Note that since our empi-rules block on active=true, all patients must be active.
thePractitioner.setActive(true);
DaoMethodOutcome daoMethodOutcome = myPractitionerDao.create(thePractitioner);
thePractitioner.setId(daoMethodOutcome.getId());
return thePractitioner;
}
@Nonnull
protected Patient buildPatientWithNameAndId(String theGivenName, String theId) {
@ -235,6 +248,11 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
return buildPatientWithNameAndId(NAME_GIVEN_PAUL, PAUL_ID);
}
@Nonnull
protected Patient buildFrankPatient() {
return buildPatientWithNameAndId(NAME_GIVEN_FRANK, FRANK_ID);
}
@Nonnull
protected Patient buildJaneWithBirthday(Date theToday) {
return buildPatientWithNameIdAndBirthday(NAME_GIVEN_JANE, JANE_ID, theToday);

View File

@ -26,7 +26,6 @@ public abstract class BaseLinkR4Test extends BaseProviderR4Test {
protected StringType myPersonId;
protected StringType myVersionlessPersonId;
@Override
@BeforeEach
public void before() {
@ -51,8 +50,10 @@ public abstract class BaseLinkR4Test extends BaseProviderR4Test {
return myEmpiLinkDaoSvc.findEmpiLinkByTarget(myPatient).get();
}
@Nonnull
protected List<EmpiLink> getPatientLinks() {
return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPatient);
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
@ -33,6 +35,10 @@ public abstract class BaseProviderR4Test extends BaseEmpiR4Test {
private IResourceLoader myResourceLoader;
@Autowired
private IEmpiSettings myEmpiSettings;
@Autowired
private IEmpiResetSvc myEmpiExpungeSvc;
@Autowired
private IEmpiBatchSvc myEmpiBatchSvc;
private String defaultScript;
@ -46,7 +52,7 @@ public abstract class BaseProviderR4Test extends BaseEmpiR4Test {
@BeforeEach
public void before() {
myEmpiProviderR4 = new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader);
myEmpiProviderR4 = new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiExpungeSvc, myEmpiBatchSvc);
defaultScript = ((EmpiSettings)myEmpiSettings).getScriptText();
}
@AfterEach

View File

@ -0,0 +1,127 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.test.concurrency.PointcutLatch;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiProviderBatchR4Test extends BaseLinkR4Test {
protected Practitioner myPractitioner;
protected StringType myPractitionerId;
protected Person myPractitionerPerson;
protected StringType myPractitionerPersonId;
@Autowired
IInterceptorService myInterceptorService;
PointcutLatch afterEmpiLatch = new PointcutLatch(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED);
@BeforeEach
public void before() {
super.before();
myPractitioner = createPractitionerAndUpdateLinks(new Practitioner());
myPractitionerId = new StringType(myPractitioner.getIdElement().getValue());
myPractitionerPerson = getPersonFromTarget(myPractitioner);
myPractitionerPersonId = new StringType(myPractitionerPerson.getIdElement().getValue());
myInterceptorService.registerAnonymousInterceptor(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, afterEmpiLatch);
}
@AfterEach
public void after() throws IOException {
myInterceptorService.unregisterInterceptor(afterEmpiLatch);
super.after();
}
@Test
public void testBatchRunOnAllPractitioners() throws InterruptedException {
StringType criteria = null;
myEmpiProviderR4.clearEmpiLinks(null);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPractitionerType(criteria, null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnSpecificPractitioner() throws InterruptedException {
myEmpiProviderR4.clearEmpiLinks(null);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPractitionerInstance(myPractitioner.getIdElement(), null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnNonExistentSpecificPractitioner() {
myEmpiProviderR4.clearEmpiLinks(null);
try {
myEmpiProviderR4.empiBatchPractitionerInstance(new IdType("Practitioner/999"), null);
fail();
} catch (ResourceNotFoundException e){}
}
@Test
public void testBatchRunOnAllPatients() throws InterruptedException {
assertLinkCount(2);
StringType criteria = null;
myEmpiProviderR4.clearEmpiLinks(null);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPatientType(criteria, null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnSpecificPatient() throws InterruptedException {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPatientInstance(myPatient.getIdElement(), null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnNonExistentSpecificPatient() {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null);
try {
myEmpiProviderR4.empiBatchPatientInstance(new IdType("Patient/999"), null);
fail();
} catch (ResourceNotFoundException e){}
}
@Test
public void testBatchRunOnAllTypes() throws InterruptedException {
assertLinkCount(2);
StringType criteria = new StringType("");
myEmpiProviderR4.clearEmpiLinks(null);
afterEmpiLatch.runWithExpectedCount(2, () -> {
myEmpiProviderR4.empiBatchOnAllTargets(criteria, null);
});
assertLinkCount(2);
}
@Test
public void testBatchRunOnAllTypesWithInvalidCriteria() {
assertLinkCount(2);
StringType criteria = new StringType("death-date=2020-06-01");
myEmpiProviderR4.clearEmpiLinks(null);
try {
myEmpiProviderR4.empiBatchPractitionerType(criteria, null);
fail();
} catch(InvalidRequestException e) {
assertThat(e.getMessage(), is(equalTo("Failed to parse match URL[death-date=2020-06-01] - Resource type Practitioner does not have a parameter with name: death-date")));
}
}
}

View File

@ -0,0 +1,170 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
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.fail;
public class EmpiProviderClearLinkR4Test extends BaseLinkR4Test {
protected Practitioner myPractitioner;
protected StringType myPractitionerId;
protected Person myPractitionerPerson;
protected StringType myPractitionerPersonId;
@BeforeEach
public void before() {
super.before();
myPractitioner = createPractitionerAndUpdateLinks(new Practitioner());
myPractitionerId = new StringType(myPractitioner.getIdElement().getValue());
myPractitionerPerson = getPersonFromTarget(myPractitioner);
myPractitionerPersonId = new StringType(myPractitionerPerson.getIdElement().getValue());
}
@Test
public void testClearAllLinks() {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null);
assertNoLinksExist();
}
private void assertNoLinksExist() {
assertNoPatientLinksExist();
assertNoPractitionerLinksExist();
}
private void assertNoPatientLinksExist() {
assertThat(getPatientLinks(), hasSize(0));
}
private void assertNoPractitionerLinksExist() {
assertThat(getPractitionerLinks(), hasSize(0));
}
@Test
public void testClearPatientLinks() {
assertLinkCount(2);
Person read = myPersonDao.read(new IdDt(myPersonId.getValueAsString()).toVersionless());
assertThat(read, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(new StringType("Patient"));
assertNoPatientLinksExist();
try {
myPersonDao.read(new IdDt(myPersonId.getValueAsString()).toVersionless());
fail();
} catch (ResourceNotFoundException e) {}
}
@Test
public void testPersonsWithMultipleHistoricalVersionsCanBeDeleted() {
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildJanePatient());
Person person = getPersonFromTarget(patientAndUpdateLinks);
assertThat(person, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(null);
assertNoPatientLinksExist();
person = getPersonFromTarget(patientAndUpdateLinks);
assertThat(person, is(nullValue()));
}
@Test
public void testPersonWithLinksToOtherPersonsCanBeDeleted() {
createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient());
Person personFromTarget = getPersonFromTarget(patientAndUpdateLinks);
Person personFromTarget2 = getPersonFromTarget(patientAndUpdateLinks1);
linkPersons(personFromTarget, personFromTarget2);
//SUT
myEmpiProviderR4.clearEmpiLinks(null);
assertNoPatientLinksExist();
IBundleProvider search = myPersonDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertThat(search.size(), is(equalTo(0)));
}
@Test
public void testPersonsWithCircularReferenceCanBeCleared() {
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient());
Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks2 = createPatientAndUpdateLinks(buildFrankPatient());
Person personFromTarget = getPersonFromTarget(patientAndUpdateLinks);
Person personFromTarget1 = getPersonFromTarget(patientAndUpdateLinks1);
Person personFromTarget2 = getPersonFromTarget(patientAndUpdateLinks2);
// A -> B -> C -> A linkages.
linkPersons(personFromTarget, personFromTarget1);
linkPersons(personFromTarget1, personFromTarget2);
linkPersons(personFromTarget2, personFromTarget);
//SUT
Parameters parameters = myEmpiProviderR4.clearEmpiLinks(null);
assertNoPatientLinksExist();
IBundleProvider search = myPersonDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertThat(search.size(), is(equalTo(0)));
}
private void linkPersons(Person theSourcePerson, Person theTargetPerson) {
Person.PersonLinkComponent plc1 = new Person.PersonLinkComponent();
plc1.setAssurance(Person.IdentityAssuranceLevel.LEVEL2);
plc1.setTarget(new Reference(theTargetPerson.getIdElement().toUnqualifiedVersionless()));
theSourcePerson.getLink().add(plc1);
myPersonDao.update(theSourcePerson);
}
@Test
public void testClearPractitionerLinks() {
assertLinkCount(2);
Person read = myPersonDao.read(new IdDt(myPractitionerPersonId.getValueAsString()).toVersionless());
assertThat(read, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(new StringType("Practitioner"));
assertNoPractitionerLinksExist();
try {
myPersonDao.read(new IdDt(myPractitionerPersonId.getValueAsString()).toVersionless());
fail();
} catch (ResourceNotFoundException e) {}
}
@Test
public void testClearInvalidTargetType() {
try {
myEmpiProviderR4.clearEmpiLinks(new StringType("Observation"));
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), is(equalTo("$empi-clear does not support resource type: Observation")));
}
}
@Nonnull
protected List<EmpiLink> getPractitionerLinks() {
return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPractitioner);
}
}

View File

@ -0,0 +1,99 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.test.concurrency.PointcutLatch;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Date;
class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
@Autowired
IEmpiBatchSvc myEmpiBatchSvc;
@Autowired
IInterceptorService myInterceptorService;
PointcutLatch afterEmpiLatch = new PointcutLatch(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED);
@BeforeEach
public void before() {
myInterceptorService.registerAnonymousInterceptor(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, afterEmpiLatch);
}
@AfterEach
public void after() throws IOException {
myInterceptorService.unregisterInterceptor(afterEmpiLatch);
afterEmpiLatch.clear();
super.after();
}
@Test
public void testEmpiBatchRunWorksOverMultipleTargetTypes() throws InterruptedException {
for (int i =0; i < 10; i++) {
createPatient(buildJanePatient());
}
for(int i = 0; i< 10; i++) {
createPractitioner(buildPractitionerWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(20, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes(null));
assertLinkCount(20);
}
@Test
public void testEmpiBatchOnPatientType() throws Exception {
for (int i =0; i < 10; i++) {
createPatient(buildPatientWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiBatchSvc.runEmpiOnTargetType("Patient", null));
assertLinkCount(10);
}
@Test
public void testEmpiBatchOnPractitionerType() throws Exception {
for (int i =0; i < 10; i++) {
createPractitioner(buildPractitionerWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes(null));
assertLinkCount(10);
}
@Test
public void testEmpiOnTargetTypeWithCriteria() throws InterruptedException {
createPatient(buildPatientWithNameIdAndBirthday("gary", "gary_id", new Date()));
createPatient(buildPatientWithNameIdAndBirthday("john", "john_id", DateUtils.addDays(new Date(), -300)));
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes("Patient?name=gary"));
assertLinkCount(1);
}
}

View File

@ -145,7 +145,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
createEmpiLink(myFromPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(myToPerson);
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
@ -156,7 +156,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(myToPerson);
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
@ -172,7 +172,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(myToPerson);
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}
@ -188,7 +188,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(myToPerson);
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}
@ -203,7 +203,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
saveLink(toLink);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPersonId(myToPerson);
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}

View File

@ -102,7 +102,6 @@ public class AddForeignKeyTask extends BaseTableColumnTask {
throw e;
}
}
}
@Override

View File

@ -138,6 +138,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
empiLink.addColumn("20200715.5", "SCORE").nullable().type(ColumnTypeEnum.FLOAT);
init510_20200725();
//EMPI Target Type
empiLink.addColumn("20200727.1","TARGET_TYPE").nullable().type(ColumnTypeEnum.STRING, EmpiLink.TARGET_TYPE_LENGTH);
}
protected void init510_20200725() {

View File

@ -160,7 +160,6 @@ public class ResourceModifiedMessage extends BaseResourceMessage implements IRes
UPDATE,
DELETE,
MANUALLY_TRIGGERED
}
private static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) {

View File

@ -0,0 +1,71 @@
package ca.uhn.fhir.empi.api;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IIdType;
public interface IEmpiBatchSvc {
/**
* Submit all eligible resources for EMPI processing.
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.
* NOTE:
* When using this function, the criteria supplied must be valid for all EMPI types. e.g. , if you
* run this with the criteria birthDate=1990-06-28, it will fail, as Practitioners do not have a birthday.
* Use with caution.
*
* @return
*/
long runEmpiOnAllTargetTypes(String theCriteria);
/**
* Given a type and a search criteria, submit all found resources for EMPI processing.
*
* @param theTargetType the resource type that you wish to execute a search over for submission to EMPI.
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing..
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnTargetType(String theTargetType, String theCriteria);
/**
* Convenience method that calls {@link #runEmpiOnTargetType(String, String)} with the type pre-populated.
*
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnPractitionerType(String theCriteria);
/**
* Convenience method that calls {@link #runEmpiOnTargetType(String, String)} with the type pre-populated.
*
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnPatientType(String theCriteria);
/**
* Given an ID and a target type valid for EMPI, manually submit the given ID for EMPI processing.
* @param theId the ID of the resource to process for EMPI.
* @return the constant `1`, as if this function returns successfully, it will have processed one resource for EMPI.
*/
long runEmpiOnTarget(IIdType theId);
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.empi.api;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IEmpiChannelSubmitterSvc {
/**
* Given an IBaseResource, submit it to the EMPI channel for processing.
*
* @param theResource the {@link IBaseResource} that should have EMPI processing applied to it.
*/
void submitResourceToEmpiChannel(IBaseResource theResource);
}

View File

@ -28,4 +28,5 @@ public interface IEmpiLinkQuerySvc {
IBaseParameters queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext);
IBaseParameters getPossibleDuplicates(EmpiTransactionContext theEmpiContext);
}

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.empi.api;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IEmpiResetSvc {
/**
* Given a resource type, delete the underlying EMPI links, and their related person objects.
*
* @param theResourceType The type of resources
*
* @return the count of deleted EMPI links
*/
long expungeAllEmpiLinksOfTargetType(String theResourceType);
/**
* Delete all EMPI links, and their related Person objects.
*
*
* @return the count of deleted EMPI links
*/
long removeAllEmpiLinks();
}

View File

@ -23,10 +23,13 @@ package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -34,11 +37,14 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.validation.IResourceLoader;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.DecimalType;
import org.hl7.fhir.dstu3.model.InstantType;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Person;
import org.hl7.fhir.dstu3.model.Practitioner;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.instance.model.api.IAnyResource;
@ -52,6 +58,8 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
private final IEmpiPersonMergerSvc myPersonMergerSvc;
private final IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private final IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
private final IEmpiResetSvc myEmpiResetSvc;
private final IEmpiBatchSvc myEmpiBatchSvc;
/**
* Constructor
@ -59,12 +67,14 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
* Note that this is not a spring bean. Any necessary injections should
* happen in the constructor
*/
public EmpiProviderDstu3(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader) {
public EmpiProviderDstu3(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader, IEmpiResetSvc theEmpiResetSvc, IEmpiBatchSvc theEmpiBatchSvc) {
super(theFhirContext, theResourceLoader);
myEmpiMatchFinderSvc = theEmpiMatchFinderSvc;
myPersonMergerSvc = thePersonMergerSvc;
myEmpiLinkUpdaterSvc = theEmpiLinkUpdaterSvc;
myEmpiLinkQuerySvc = theEmpiLinkQuerySvc;
myEmpiResetSvc = theEmpiResetSvc;
myEmpiBatchSvc = theEmpiBatchSvc;
}
@Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class)
@ -149,4 +159,88 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
return (Parameters) myEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, createEmpiContext(theRequestDetails));
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class)
})
public Parameters empiBatchOnAllTargets(
@OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria,
ServletRequestDetails theRequestDetails) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnAllTargetTypes(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
private String convertCriteriaToString(StringType theCriteria) {
return theCriteria == null ? null : theCriteria.getValueAsString();
}
@Operation(name = ProviderConstants.EMPI_CLEAR, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class)
})
public Parameters clearEmpiLinks(@OperationParam(name=ProviderConstants.EMPI_CLEAR_TARGET_TYPE, min = 0, max = 1) StringType theTargetType) {
long resetCount;
if (theTargetType == null || StringUtils.isBlank(theTargetType.getValue())) {
resetCount = myEmpiResetSvc.removeAllEmpiLinks();
} else {
resetCount = myEmpiResetSvc.expungeAllEmpiLinksOfTargetType(theTargetType.getValueNotNull());
}
Parameters parameters = new Parameters();
parameters.addParameter().setName(ProviderConstants.OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT)
.setValue(new DecimalType(resetCount));
return parameters;
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPatientInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPatientType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPatientType(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPractitionerInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPractitionerType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPractitionerType(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
/**
* Helper function to build the out-parameters for all batch EMPI operations.
*/
private Parameters buildEmpiOutParametersWithCount(long theCount) {
Parameters parameters = new Parameters();
parameters.addParameter()
.setName(ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT)
.setValue(new DecimalType(theCount));
return parameters;
}
}

View File

@ -22,6 +22,8 @@ package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
@ -47,14 +49,18 @@ public class EmpiProviderLoader {
private IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
@Autowired
private IResourceLoader myResourceLoader;
@Autowired
private IEmpiResetSvc myEmpiResetSvc;
@Autowired
private IEmpiBatchSvc myEmpiBatchSvc;
public void loadProvider() {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
myResourceProviderFactory.addSupplier(() -> new EmpiProviderDstu3(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader));
myResourceProviderFactory.addSupplier(() -> new EmpiProviderDstu3(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiResetSvc, myEmpiBatchSvc));
break;
case R4:
myResourceProviderFactory.addSupplier(() -> new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader));
myResourceProviderFactory.addSupplier(() -> new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiResetSvc, myEmpiBatchSvc));
break;
default:
throw new ConfigurationException("EMPI not supported for FHIR version " + myFhirContext.getVersion().getVersion());

View File

@ -23,10 +23,13 @@ package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -34,13 +37,17 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.validation.IResourceLoader;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
@ -52,6 +59,8 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
private final IEmpiPersonMergerSvc myPersonMergerSvc;
private final IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private final IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
private final IEmpiResetSvc myEmpiExpungeSvc;
private final IEmpiBatchSvc myEmpiBatchSvc;
/**
* Constructor
@ -59,12 +68,14 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
* Note that this is not a spring bean. Any necessary injections should
* happen in the constructor
*/
public EmpiProviderR4(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader) {
public EmpiProviderR4(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader, IEmpiResetSvc theEmpiExpungeSvc, IEmpiBatchSvc theEmpiBatchSvc) {
super(theFhirContext, theResourceLoader);
myEmpiMatchFinderSvc = theEmpiMatchFinderSvc;
myPersonMergerSvc = thePersonMergerSvc;
myEmpiLinkUpdaterSvc = theEmpiLinkUpdaterSvc;
myEmpiLinkQuerySvc = theEmpiLinkQuerySvc;
myEmpiExpungeSvc = theEmpiExpungeSvc;
myEmpiBatchSvc = theEmpiBatchSvc;
}
@Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class)
@ -117,6 +128,22 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
return (Person) myEmpiLinkUpdaterSvc.updateLink(person, target, matchResult, createEmpiContext(theRequestDetails));
}
@Operation(name = ProviderConstants.EMPI_CLEAR, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type=DecimalType.class)
})
public Parameters clearEmpiLinks(@OperationParam(name=ProviderConstants.EMPI_CLEAR_TARGET_TYPE, min = 0, max = 1) StringType theTargetType) {
long resetCount;
if (theTargetType == null || StringUtils.isBlank(theTargetType.getValue())) {
resetCount = myEmpiExpungeSvc.removeAllEmpiLinks();
} else {
resetCount = myEmpiExpungeSvc.expungeAllEmpiLinksOfTargetType(theTargetType.getValueNotNull());
}
Parameters parameters = new Parameters();
parameters.addParameter().setName(ProviderConstants.OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT)
.setValue(new DecimalType(resetCount));
return parameters;
}
@Operation(name = ProviderConstants.EMPI_QUERY_LINKS, idempotent = true)
public Parameters queryLinks(@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, min = 0, max = 1) StringType thePersonId,
@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, min = 0, max = 1) StringType theTargetId,
@ -149,4 +176,72 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
return (Parameters) myEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, createEmpiContext(theRequestDetails));
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= IntegerType.class)
})
public Parameters empiBatchOnAllTargets(
@OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria,
ServletRequestDetails theRequestDetails) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnAllTargetTypes(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
private String convertCriteriaToString(StringType theCriteria) {
return theCriteria == null ? null : theCriteria.getValueAsString();
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPatientInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPatientType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPatientType(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPractitionerInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPractitionerType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPractitionerType(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
/**
* Helper function to build the out-parameters for all batch EMPI operations.
*/
private Parameters buildEmpiOutParametersWithCount(long theCount) {
Parameters parameters = new Parameters();
parameters.addParameter()
.setName(ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT)
.setValue(new DecimalType(theCount));
return parameters;
}
}

View File

@ -81,4 +81,11 @@ public class ProviderConstants {
public static final String EMPI_DUPLICATE_PERSONS = "$empi-duplicate-persons";
public static final String EMPI_NOT_DUPLICATE = "$empi-not-duplicate";
public static final String EMPI_CLEAR = "$empi-clear";
public static final String EMPI_CLEAR_TARGET_TYPE = "targetType";
public static final String OPERATION_EMPI_BATCH_RUN = "$empi-submit";
public static final String EMPI_BATCH_RUN_CRITERIA= "criteria" ;
public static final String OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT = "submitted" ;
public static final String OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT = "deleted";
}

View File

@ -67,6 +67,12 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
myPointcut = null;
}
public void runWithExpectedCount(int theExpectedCount, Runnable r) throws InterruptedException {
this.setExpectedCount(theExpectedCount);
r.run();
this.awaitExpected();
}
public long getLastInvoke() {
return myLastInvoke.get();
}