Patient ID Partition Mode should support conditional create (#2828)
* Fixes * Test fix * Add changelog * Test fixes * Test fixes * Docs tweak * Partitioning fixes * Import fix * Fix test data
This commit is contained in:
parent
6bbb403fd1
commit
f957661dda
|
@ -21,20 +21,29 @@ package ca.uhn.fhir.interceptor.model;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ReadPartitionIdRequestDetails {
|
||||
|
||||
private final String myResourceType;
|
||||
private final RestOperationTypeEnum myRestOperationType;
|
||||
private final IIdType myReadResourceId;
|
||||
private final Object mySearchParams;
|
||||
private final IBaseResource myConditionalTargetOrNull;
|
||||
|
||||
public ReadPartitionIdRequestDetails(String theResourceType, RestOperationTypeEnum theRestOperationType, IIdType theReadResourceId, Object theSearchParams) {
|
||||
public ReadPartitionIdRequestDetails(String theResourceType, RestOperationTypeEnum theRestOperationType, IIdType theReadResourceId, Object theSearchParams, @Nullable IBaseResource theConditionalTargetOrNull) {
|
||||
myResourceType = theResourceType;
|
||||
myRestOperationType = theRestOperationType;
|
||||
myReadResourceId = theReadResourceId;
|
||||
mySearchParams = theSearchParams;
|
||||
myConditionalTargetOrNull = theConditionalTargetOrNull;
|
||||
}
|
||||
|
||||
public IBaseResource getConditionalTargetOrNull() {
|
||||
return myConditionalTargetOrNull;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
|
@ -55,11 +64,11 @@ public class ReadPartitionIdRequestDetails {
|
|||
|
||||
public static ReadPartitionIdRequestDetails forRead(String theResourceType, IIdType theId, boolean theIsVread) {
|
||||
RestOperationTypeEnum op = theIsVread ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, op, theId.withResourceType(theResourceType), null);
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, op, theId.withResourceType(theResourceType), null, null);
|
||||
}
|
||||
|
||||
public static ReadPartitionIdRequestDetails forSearchType(String theResourceType, Object theParams) {
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, theParams);
|
||||
public static ReadPartitionIdRequestDetails forSearchType(String theResourceType, Object theParams, IBaseResource theConditionalOperationTargetOrNull) {
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, theParams, theConditionalOperationTargetOrNull);
|
||||
}
|
||||
|
||||
public static ReadPartitionIdRequestDetails forHistory(String theResourceType, IIdType theIdType) {
|
||||
|
@ -71,6 +80,6 @@ public class ReadPartitionIdRequestDetails {
|
|||
} else {
|
||||
restOperationTypeEnum = RestOperationTypeEnum.HISTORY_SYSTEM;
|
||||
}
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, restOperationTypeEnum, theIdType, null);
|
||||
return new ReadPartitionIdRequestDetails(theResourceType, restOperationTypeEnum, theIdType, null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2828
|
||||
title: "PatientIdPartitionInterceptor now supports conditional creates of resources where the resource is in
|
||||
the patient compartment but the conditional URL does not contain a patietn reference."
|
|
@ -47,6 +47,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
|
@ -211,7 +212,21 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
|
|||
/**
|
||||
* Search for IDs for processing a match URLs, etc.
|
||||
*/
|
||||
Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest);
|
||||
default Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest) {
|
||||
return searchForIds(theParams, theRequest, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for IDs for processing a match URLs, etc.
|
||||
*
|
||||
* @param theConditionalOperationTargetOrNull If we're searching for IDs in order to satisfy a conditional
|
||||
* create/update, this is the resource being searched for
|
||||
* @since 5.5.0
|
||||
*/
|
||||
default Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) {
|
||||
return searchForIds(theParams, theRequest);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes a map of incoming raw search parameters and translates/parses them into
|
||||
|
@ -262,6 +277,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
|
|||
|
||||
/**
|
||||
* Delete a list of resource Pids
|
||||
*
|
||||
* @param theUrl the original URL that triggered the delete
|
||||
* @param theResourceIds the ids of the resources to be deleted
|
||||
* @param theDeleteConflicts out parameter of conflicts preventing deletion
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.bulk.export.job;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
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;
|
||||
|
@ -101,7 +102,7 @@ public class ResourceToFileWriter implements ItemWriter<List<IBaseResource>> {
|
|||
IBaseBinary binary = BinaryUtil.newBinary(myFhirContext);
|
||||
binary.setContentType(Constants.CT_FHIR_NDJSON);
|
||||
binary.setContent(myOutputStream.toByteArray());
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, new SystemRequestDetails());
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
return outcome.getResource().getIdElement();
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,7 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter
|
|||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.persistence.NoResultException;
|
||||
|
@ -569,7 +570,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
SearchParameterMap paramMap = resourceSearch.getSearchParameterMap();
|
||||
paramMap.setLoadSynchronous(true);
|
||||
|
||||
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest);
|
||||
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest, null);
|
||||
|
||||
if (resourceIds.size() > 1) {
|
||||
if (!getConfig().isAllowMultipleDelete()) {
|
||||
|
@ -1415,7 +1416,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
|
||||
}
|
||||
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, null);
|
||||
IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId);
|
||||
|
||||
if (retVal instanceof PersistedJpaBundleProvider) {
|
||||
|
@ -1490,7 +1491,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
|
||||
@Override
|
||||
public Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest) {
|
||||
public Set<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) {
|
||||
TransactionDetails transactionDetails = new TransactionDetails();
|
||||
|
||||
return myTransactionService.execute(theRequest, transactionDetails, tx -> {
|
||||
|
@ -1506,9 +1507,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
HashSet<ResourcePersistentId> retVal = new HashSet<>();
|
||||
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, theConditionalOperationTargetOrNull);
|
||||
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams);
|
||||
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
|
||||
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
|
||||
while (iter.hasNext()) {
|
||||
retVal.add(iter.next());
|
||||
|
@ -1616,7 +1617,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
IIdType resourceId;
|
||||
if (isNotBlank(theMatchUrl)) {
|
||||
Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest);
|
||||
Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource);
|
||||
if (match.size() > 1) {
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
|
||||
throw new PreconditionFailedException(msg);
|
||||
|
|
|
@ -91,7 +91,7 @@ public abstract class BaseHapiFhirResourceDaoObservation<T extends IBaseResource
|
|||
TreeMap<Long, IQueryParameterType> orderedSubjectReferenceMap = new TreeMap<>();
|
||||
if(theSearchParameterMap.containsKey(getSubjectParamName())) {
|
||||
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap, null);
|
||||
|
||||
List<List<IQueryParameterType>> patientParams = new ArrayList<>();
|
||||
if (theSearchParameterMap.get(getPatientParamName()) != null) {
|
||||
|
|
|
@ -79,7 +79,7 @@ public class FhirResourceDaoPatientDstu2 extends BaseHapiFhirResourceDao<Patient
|
|||
paramMap.setLoadSynchronous(true);
|
||||
}
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,13 @@ public class MatchResourceUrlService {
|
|||
* Note that this will only return a maximum of 2 results!!
|
||||
*/
|
||||
public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, TransactionDetails theTransactionDetails, RequestDetails theRequest) {
|
||||
return processMatchUrl(theMatchUrl, theResourceType, theTransactionDetails, theRequest, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this will only return a maximum of 2 results!!
|
||||
*/
|
||||
public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, TransactionDetails theTransactionDetails, RequestDetails theRequest, IBaseResource theConditionalOperationTargetOrNull) {
|
||||
String resourceType = myContext.getResourceType(theResourceType);
|
||||
String matchUrl = massageForStorage(resourceType, theMatchUrl);
|
||||
|
||||
|
@ -92,7 +99,7 @@ public class MatchResourceUrlService {
|
|||
}
|
||||
paramMap.setLoadSynchronousUpTo(2);
|
||||
|
||||
Set<ResourcePersistentId> retVal = search(paramMap, theResourceType, theRequest);
|
||||
Set<ResourcePersistentId> retVal = search(paramMap, theResourceType, theRequest, theConditionalOperationTargetOrNull);
|
||||
|
||||
if (myDaoConfig.isMatchUrlCacheEnabled() && retVal.size() == 1) {
|
||||
ResourcePersistentId pid = retVal.iterator().next();
|
||||
|
@ -124,14 +131,14 @@ public class MatchResourceUrlService {
|
|||
return existing;
|
||||
}
|
||||
|
||||
public <R extends IBaseResource> Set<ResourcePersistentId> search(SearchParameterMap theParamMap, Class<R> theResourceType, RequestDetails theRequest) {
|
||||
public <R extends IBaseResource> Set<ResourcePersistentId> search(SearchParameterMap theParamMap, Class<R> theResourceType, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) {
|
||||
StopWatch sw = new StopWatch();
|
||||
IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType);
|
||||
if (dao == null) {
|
||||
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
|
||||
}
|
||||
|
||||
Set<ResourcePersistentId> retVal = dao.searchForIds(theParamMap, theRequest);
|
||||
Set<ResourcePersistentId> retVal = dao.searchForIds(theParamMap, theRequest, theConditionalOperationTargetOrNull);
|
||||
|
||||
// Interceptor broadcast: JPA_PERFTRACE_INFO
|
||||
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
|
||||
|
|
|
@ -48,7 +48,7 @@ public class FhirResourceDaoObservationDstu3 extends BaseHapiFhirResourceDaoObse
|
|||
|
||||
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ public class FhirResourceDaoPatientDstu3 extends BaseHapiFhirResourceDao<Patient
|
|||
paramMap.setLoadSynchronous(true);
|
||||
}
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ public class FhirResourceDaoObservationR4 extends BaseHapiFhirResourceDaoObserva
|
|||
public IBundleProvider observationsLastN(SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
|
||||
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
|
||||
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ public class FhirResourceDaoPatientR4 extends BaseHapiFhirResourceDao<Patient>im
|
|||
paramMap.setLoadSynchronous(true);
|
||||
}
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ public class FhirResourceDaoObservationR5 extends BaseHapiFhirResourceDaoObserva
|
|||
|
||||
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ public class FhirResourceDaoPatientR5 extends BaseHapiFhirResourceDao<Patient> i
|
|||
paramMap.setLoadSynchronous(true);
|
||||
}
|
||||
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
|
||||
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap, null);
|
||||
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
|
||||
}
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter
|
|||
List<RequestPartitionId> retval = new ArrayList<>();
|
||||
for (String url : theUrlsToDeleteExpunge) {
|
||||
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(url);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, resourceSearch.getResourceName(), null);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, resourceSearch.getResourceName(), null, null);
|
||||
retval.add(requestPartitionId);
|
||||
}
|
||||
return retval;
|
||||
|
|
|
@ -118,17 +118,6 @@ public class PatientIdPartitionInterceptor {
|
|||
return provideCompartmentMemberInstanceResponse(theRequestDetails, compartmentIdentity);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<RuntimeSearchParam> getCompartmentSearchParams(RuntimeResourceDefinition resourceDef) {
|
||||
return resourceDef
|
||||
.getSearchParams()
|
||||
.stream()
|
||||
.filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
|
||||
.filter(param -> param.getProvidesMembershipInCompartments() != null && param.getProvidesMembershipInCompartments().contains("Patient"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
|
||||
public RequestPartitionId identifyForRead(ReadPartitionIdRequestDetails theReadDetails, RequestDetails theRequestDetails) {
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theReadDetails.getResourceType());
|
||||
|
@ -169,7 +158,23 @@ public class PatientIdPartitionInterceptor {
|
|||
// nothing
|
||||
}
|
||||
|
||||
return provideUnsupportedQueryResponse(theReadDetails);
|
||||
// If we couldn't identify a patient ID by the URL, let's try using the
|
||||
// conditional target if we have one
|
||||
if (theReadDetails.getConditionalTargetOrNull() != null) {
|
||||
return identifyForCreate(theReadDetails.getConditionalTargetOrNull(), theRequestDetails);
|
||||
}
|
||||
|
||||
return provideNonPatientSpecificQueryResponse(theReadDetails);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<RuntimeSearchParam> getCompartmentSearchParams(RuntimeResourceDefinition resourceDef) {
|
||||
return resourceDef
|
||||
.getSearchParams()
|
||||
.stream()
|
||||
.filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
|
||||
.filter(param -> param.getProvidesMembershipInCompartments() != null && param.getProvidesMembershipInCompartments().contains("Patient"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private String getSingleResourceIdValueOrNull(SearchParameterMap theParams, String theParamName, String theResourceType) {
|
||||
|
@ -206,8 +211,8 @@ public class PatientIdPartitionInterceptor {
|
|||
/**
|
||||
* Return a partition or throw an error for FHIR operations that can not be used with this interceptor
|
||||
*/
|
||||
protected RequestPartitionId provideUnsupportedQueryResponse(ReadPartitionIdRequestDetails theRequestDetails) {
|
||||
throw new MethodNotAllowedException("This server is not able to handle this request of type " + theRequestDetails.getRestOperationType());
|
||||
protected RequestPartitionId provideNonPatientSpecificQueryResponse(ReadPartitionIdRequestDetails theRequestDetails) {
|
||||
return RequestPartitionId.allPartitions();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -27,13 +27,13 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
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.dao.data.INpmPackageVersionDao;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistryController;
|
||||
|
@ -63,6 +63,7 @@ import org.springframework.transaction.PlatformTransactionManager;
|
|||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.hl7.fhir.utilities.npm.NpmPackage;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -365,6 +366,13 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
|
|||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private SystemRequestDetails newSystemRequestDetails() {
|
||||
return
|
||||
new SystemRequestDetails()
|
||||
.setRequestPartitionId(RequestPartitionId.defaultPartition());
|
||||
}
|
||||
|
||||
private void createResource(IFhirResourceDao theDao, IBaseResource theResource) {
|
||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||
SystemRequestDetails requestDetails = newSystemRequestDetails();
|
||||
|
|
|
@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.partition;
|
|||
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
@ -44,8 +45,8 @@ public interface IRequestPartitionHelperSvc {
|
|||
}
|
||||
|
||||
@Nonnull
|
||||
default RequestPartitionId determineReadPartitionForRequestForSearchType(RequestDetails theRequest, String theResourceType, SearchParameterMap theParams) {
|
||||
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType(theResourceType, theParams);
|
||||
default RequestPartitionId determineReadPartitionForRequestForSearchType(RequestDetails theRequest, String theResourceType, SearchParameterMap theParams, IBaseResource theConditionalOperationTargetOrNull) {
|
||||
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType(theResourceType, theParams, theConditionalOperationTargetOrNull);
|
||||
return determineReadPartitionForRequest(theRequest, theResourceType, details);
|
||||
}
|
||||
|
||||
|
|
|
@ -109,14 +109,14 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||
// Handle system requests
|
||||
//TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through SystemRequestDetails instead.
|
||||
if (theRequest == null && nonPartitionableResource) {
|
||||
if ((theRequest == null || theRequest instanceof SystemRequestDetails) && nonPartitionableResource) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
if (theRequest instanceof SystemRequestDetails) {
|
||||
if (theRequest instanceof SystemRequestDetails && systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) {
|
||||
requestPartitionId = getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource);
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_READ
|
||||
} else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) {
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_READ
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||
|
@ -186,15 +186,16 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
|
||||
|
||||
//TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through SystemRequestDetails instead.
|
||||
if (theRequest == null && nonPartitionableResource) {
|
||||
if ((theRequest == null || theRequest instanceof SystemRequestDetails) && nonPartitionableResource) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
if (theRequest instanceof SystemRequestDetails) {
|
||||
if (theRequest instanceof SystemRequestDetails && systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) {
|
||||
requestPartitionId = getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource);
|
||||
} else {
|
||||
//This is an external Request (e.g. ServletRequestDetails) so we want to figure out the partition via interceptor.
|
||||
HookParams params = new HookParams()// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||
HookParams params = new HookParams()
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
|
@ -215,6 +216,10 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
return RequestPartitionId.allPartitions();
|
||||
}
|
||||
|
||||
private boolean systemRequestHasExplicitPartition(@Nonnull SystemRequestDetails theRequest) {
|
||||
return theRequest.getRequestPartitionId() != null || theRequest.getTenantId() != null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId) {
|
||||
|
|
|
@ -72,8 +72,9 @@ public class SystemRequestDetails extends RequestDetails {
|
|||
return myRequestPartitionId;
|
||||
}
|
||||
|
||||
public void setRequestPartitionId(RequestPartitionId theRequestPartitionId) {
|
||||
public SystemRequestDetails setRequestPartitionId(RequestPartitionId theRequestPartitionId) {
|
||||
myRequestPartitionId = theRequestPartitionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -271,7 +271,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
String resourceType = search.getResourceType();
|
||||
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
|
||||
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, resourceType, params);
|
||||
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, resourceType, params, null);
|
||||
SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequestDetails, requestPartitionId);
|
||||
myIdToSearchTask.put(search.getUuid(), task);
|
||||
myExecutor.submit(task);
|
||||
|
|
|
@ -814,7 +814,9 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
if (findVersionFieldName != null) {
|
||||
sqlBuilder.append(", r." + findVersionFieldName);
|
||||
}
|
||||
sqlBuilder.append(" FROM ResourceLink r WHERE r.");
|
||||
sqlBuilder.append(" FROM ResourceLink r WHERE ");
|
||||
|
||||
sqlBuilder.append("r.");
|
||||
sqlBuilder.append(searchPidFieldName);
|
||||
sqlBuilder.append(" IN (:target_pids)");
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.bulk;
|
|||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.batch.BatchJobsConfig;
|
||||
|
@ -944,7 +945,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
|
|||
|
||||
public String getBinaryContents(IBulkDataExportSvc.JobInfo theJobInfo, int theIndex) {
|
||||
// Iterate over the files
|
||||
Binary nextBinary = myBinaryDao.read(theJobInfo.getFiles().get(theIndex).getResourceId(), new SystemRequestDetails());
|
||||
Binary nextBinary = myBinaryDao.read(theJobInfo.getFiles().get(theIndex).getResourceId(), new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType());
|
||||
String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8);
|
||||
ourLog.info("Next contents for type {}:\n{}", nextBinary.getResourceType(), nextContents);
|
||||
|
@ -1238,12 +1239,12 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
|
|||
createCareTeamWithIndex(i, patId);
|
||||
}
|
||||
|
||||
myPatientGroupId = myGroupDao.update(group, new SystemRequestDetails()).getId();
|
||||
myPatientGroupId = myGroupDao.update(group, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition())).getId();
|
||||
|
||||
//Manually create another golden record
|
||||
Patient goldenPatient2 = new Patient();
|
||||
goldenPatient2.setId("PAT888");
|
||||
DaoMethodOutcome g2Outcome = myPatientDao.update(goldenPatient2, new SystemRequestDetails());
|
||||
DaoMethodOutcome g2Outcome = myPatientDao.update(goldenPatient2, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
Long goldenPid2 = myIdHelperService.getPidOrNull(g2Outcome.getResource());
|
||||
|
||||
//Create some nongroup patients MDM linked to a different golden resource. They shouldnt be included in the query.
|
||||
|
@ -1272,14 +1273,14 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
|
|||
patient.setGender(i % 2 == 0 ? Enumerations.AdministrativeGender.MALE : Enumerations.AdministrativeGender.FEMALE);
|
||||
patient.addName().setFamily("FAM" + i);
|
||||
patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i);
|
||||
return myPatientDao.update(patient, new SystemRequestDetails());
|
||||
return myPatientDao.update(patient, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
}
|
||||
|
||||
private void createCareTeamWithIndex(int i, IIdType patId) {
|
||||
CareTeam careTeam = new CareTeam();
|
||||
careTeam.setId("CT" + i);
|
||||
careTeam.setSubject(new Reference(patId)); // This maps to the "patient" search parameter on CareTeam
|
||||
myCareTeamDao.update(careTeam, new SystemRequestDetails());
|
||||
myCareTeamDao.update(careTeam, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
}
|
||||
|
||||
private void createImmunizationWithIndex(int i, IIdType patId) {
|
||||
|
@ -1297,7 +1298,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
|
|||
cc.addCoding().setSystem("vaccines").setCode("COVID-19");
|
||||
immunization.setVaccineCode(cc);
|
||||
}
|
||||
myImmunizationDao.update(immunization, new SystemRequestDetails());
|
||||
myImmunizationDao.update(immunization, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
}
|
||||
|
||||
private void createObservationWithIndex(int i, IIdType patId) {
|
||||
|
@ -1308,7 +1309,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test {
|
|||
if (patId != null) {
|
||||
obs.getSubject().setReference(patId.getValue());
|
||||
}
|
||||
myObservationDao.update(obs, new SystemRequestDetails());
|
||||
myObservationDao.update(obs, new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()));
|
||||
}
|
||||
|
||||
public void linkToGoldenResource(Long theGoldenPid, Long theSourcePid) {
|
||||
|
|
|
@ -506,6 +506,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
|
|||
|
||||
myPagingProvider.setDefaultPageSize(BasePagingProvider.DEFAULT_DEFAULT_PAGE_SIZE);
|
||||
myPagingProvider.setMaximumPageSize(BasePagingProvider.DEFAULT_MAX_PAGE_SIZE);
|
||||
|
||||
myPartitionSettings.setPartitioningEnabled(false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
|
|
@ -49,6 +49,7 @@ import static org.awaitility.Awaitility.await;
|
|||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
@ -566,12 +567,10 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
/*
|
||||
* 20 should be prefetched since that's the initial page size
|
||||
*/
|
||||
await().until(() -> {
|
||||
return runInTransaction(() -> {
|
||||
await().until(() -> runInTransaction(() -> {
|
||||
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException(""));
|
||||
return search.getNumFound() == 20;
|
||||
});
|
||||
});
|
||||
return search.getNumFound();
|
||||
}), equalTo(20));
|
||||
runInTransaction(() -> {
|
||||
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException(""));
|
||||
assertEquals(20, search.getNumFound());
|
||||
|
|
|
@ -4,15 +4,24 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
|||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4SystemTest;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.util.MultimapCollector;
|
||||
import ca.uhn.fhir.jpa.util.SqlQuery;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
|
@ -22,8 +31,18 @@ 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.either;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
@ -225,11 +244,9 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
|
|||
createObservationB();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
try {
|
||||
myObservationDao.search(SearchParameterMap.newSynchronous(), mySrd);
|
||||
} catch (MethodNotAllowedException e) {
|
||||
assertEquals("This server is not able to handle this request of type SEARCH_TYPE", e.getMessage());
|
||||
}
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertEquals("SELECT t0.RES_ID FROM HFJ_RESOURCE t0 WHERE ((t0.RES_TYPE = 'Observation') AND (t0.RES_DELETED_AT IS NULL)) limit '10'", myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -314,6 +331,139 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
|
|||
assertThat(myCaptureQueriesListener.getSelectQueries().get(1).getSql(false, false), containsString("PARTITION_ID="));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testTransaction_NoRequestDetails() throws IOException {
|
||||
Bundle input = loadResourceFromClasspath(Bundle.class, "/r4/load_bundle.json");
|
||||
|
||||
// Maybe in the future we'll make request details mandatory and if that
|
||||
// causes this to fail that's ok
|
||||
Bundle outcome = mySystemDao.transaction(null, input);
|
||||
|
||||
ListMultimap<String, String> resourceIds = outcome
|
||||
.getEntry()
|
||||
.stream()
|
||||
.collect(MultimapCollector.toMultimap(t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getResourceType(), t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getValue()));
|
||||
|
||||
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
|
||||
logAllResources();
|
||||
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t->t.getResourceType(), t->t.getPartitionId().getPartitionId()));
|
||||
});
|
||||
|
||||
assertThat(resourcesByType.get("Patient"), contains(4267));
|
||||
assertThat(resourcesByType.get("ExplanationOfBenefit"), contains(4267));
|
||||
assertThat(resourcesByType.get("Coverage"), contains(4267));
|
||||
assertThat(resourcesByType.get("Organization"), contains(-1, -1));
|
||||
assertThat(resourcesByType.get("Practitioner"), contains(-1, -1, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransaction_SystemRequestDetails() throws IOException {
|
||||
Bundle input = loadResourceFromClasspath(Bundle.class, "/r4/load_bundle.json");
|
||||
myCaptureQueriesListener.clear();
|
||||
Bundle outcome = mySystemDao.transaction(new SystemRequestDetails(), input);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
List<String> selectQueryStrings = myCaptureQueriesListener
|
||||
.getSelectQueries()
|
||||
.stream()
|
||||
.map(t -> t.getSql(false, false).toUpperCase(Locale.US))
|
||||
.filter(t -> !t.contains("FROM HFJ_TAG_DEF"))
|
||||
.collect(Collectors.toList());
|
||||
for (String next : selectQueryStrings) {
|
||||
assertThat(next, either(containsString("PARTITION_ID =")).or(containsString("PARTITION_ID IN")));
|
||||
}
|
||||
|
||||
ListMultimap<String, String> resourceIds = outcome
|
||||
.getEntry()
|
||||
.stream()
|
||||
.collect(MultimapCollector.toMultimap(t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getResourceType(), t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getValue()));
|
||||
|
||||
String patientId = resourceIds.get("Patient").get(0);
|
||||
|
||||
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
|
||||
logAllResources();
|
||||
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t->t.getResourceType(), t->t.getPartitionId().getPartitionId()));
|
||||
});
|
||||
|
||||
assertThat(resourcesByType.get("Patient"), contains(4267));
|
||||
assertThat(resourcesByType.get("ExplanationOfBenefit"), contains(4267));
|
||||
assertThat(resourcesByType.get("Coverage"), contains(4267));
|
||||
assertThat(resourcesByType.get("Organization"), contains(-1, -1));
|
||||
assertThat(resourcesByType.get("Practitioner"), contains(-1, -1, -1));
|
||||
|
||||
// Try Searching
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(ExplanationOfBenefit.SP_PATIENT, new ReferenceParam(patientId));
|
||||
map.addInclude(new Include("*"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider result = myExplanationOfBenefitDao.search(map);
|
||||
List<String> resultIds = toUnqualifiedVersionlessIdValues(result);
|
||||
assertThat(resultIds.toString(), resultIds, containsInAnyOrder(
|
||||
resourceIds.get("Coverage").get(0),
|
||||
resourceIds.get("Organization").get(0),
|
||||
resourceIds.get("ExplanationOfBenefit").get(0),
|
||||
resourceIds.get("Patient").get(0),
|
||||
resourceIds.get("Practitioner").get(0),
|
||||
resourceIds.get("Practitioner").get(1),
|
||||
resourceIds.get("Practitioner").get(2)
|
||||
));
|
||||
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
|
||||
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
|
||||
assertThat(selectQueries.get(0).getSql(true, false).toUpperCase(Locale.US), matchesPattern("SELECT.*FROM HFJ_RES_LINK.*WHERE.*PARTITION_ID = '4267'.*"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearch() throws IOException {
|
||||
Bundle input = loadResourceFromClasspath(Bundle.class, "/r4/load_bundle.json");
|
||||
Bundle outcome = mySystemDao.transaction(new SystemRequestDetails(), input);
|
||||
|
||||
ListMultimap<String, String> resourceIds = outcome
|
||||
.getEntry()
|
||||
.stream()
|
||||
.collect(MultimapCollector.toMultimap(t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getResourceType(), t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getValue()));
|
||||
|
||||
String patientId = resourceIds.get("Patient").get(0);
|
||||
|
||||
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
|
||||
logAllResources();
|
||||
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t->t.getResourceType(), t->t.getPartitionId().getPartitionId()));
|
||||
});
|
||||
|
||||
assertThat(resourcesByType.get("Patient"), contains(4267));
|
||||
assertThat(resourcesByType.get("ExplanationOfBenefit"), contains(4267));
|
||||
assertThat(resourcesByType.get("Coverage"), contains(4267));
|
||||
assertThat(resourcesByType.get("Organization"), contains(-1, -1));
|
||||
assertThat(resourcesByType.get("Practitioner"), contains(-1, -1, -1));
|
||||
|
||||
// Try Searching
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(ExplanationOfBenefit.SP_PATIENT, new ReferenceParam(patientId));
|
||||
map.addInclude(new Include("*"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider result = myExplanationOfBenefitDao.search(map);
|
||||
List<String> resultIds = toUnqualifiedVersionlessIdValues(result);
|
||||
assertThat(resultIds.toString(), resultIds, containsInAnyOrder(
|
||||
resourceIds.get("Coverage").get(0),
|
||||
resourceIds.get("Organization").get(0),
|
||||
resourceIds.get("ExplanationOfBenefit").get(0),
|
||||
resourceIds.get("Patient").get(0),
|
||||
resourceIds.get("Practitioner").get(0),
|
||||
resourceIds.get("Practitioner").get(1),
|
||||
resourceIds.get("Practitioner").get(2)
|
||||
));
|
||||
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
|
||||
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueries();
|
||||
assertThat(selectQueries.get(0).getSql(true, false).toUpperCase(Locale.US), matchesPattern("SELECT.*FROM HFJ_RES_LINK.*WHERE.*PARTITION_ID = '4267'.*"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testHistory_Type() {
|
||||
myOrganizationDao.history(null, null, null, mySrd);
|
||||
|
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
|
|||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.jpa.interceptor.PatientIdPartitionInterceptor;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
|
||||
|
@ -40,6 +41,8 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
|
|||
private IInterceptorService myInterceptorService;
|
||||
@Autowired
|
||||
private RequestTenantPartitionInterceptor myRequestTenantPartitionInterceptor;
|
||||
@Autowired
|
||||
private ISearchParamExtractor mySearchParamExtractor;
|
||||
|
||||
@AfterEach
|
||||
public void disablePartitioning() {
|
||||
|
@ -75,7 +78,7 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
|
|||
public void testSaveAndDeletePackagePartitionsEnabled() throws IOException {
|
||||
myPartitionSettings.setPartitioningEnabled(true);
|
||||
myPartitionSettings.setDefaultPartitionId(1);
|
||||
myInterceptorService.registerInterceptor(new PatientIdPartitionInterceptor());
|
||||
myInterceptorService.registerInterceptor(new PatientIdPartitionInterceptor(myFhirCtx, mySearchParamExtractor));
|
||||
myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor);
|
||||
|
||||
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/basisprofil.de.tar.gz")) {
|
||||
|
@ -109,7 +112,7 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
|
|||
myPartitionSettings.setPartitioningEnabled(true);
|
||||
myPartitionSettings.setDefaultPartitionId(0);
|
||||
myPartitionSettings.setUnnamedPartitionMode(true);
|
||||
myInterceptorService.registerInterceptor(new PatientIdPartitionInterceptor());
|
||||
myInterceptorService.registerInterceptor(new PatientIdPartitionInterceptor(myFhirCtx, mySearchParamExtractor));
|
||||
myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor);
|
||||
|
||||
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")) {
|
||||
|
|
|
@ -423,9 +423,9 @@ public class NpmR4Test extends BaseJpaR4Test {
|
|||
myDaoConfig.setAllowExternalReferences(true);
|
||||
|
||||
byte[] bytes = loadClasspathBytes("/packages/test-draft-sample.tgz");
|
||||
myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", bytes);
|
||||
myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.onlydrafts/0.11.1", bytes);
|
||||
|
||||
PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL);
|
||||
PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.onlydrafts").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL);
|
||||
PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec);
|
||||
assertEquals(0, outcome.getResourcesInstalled().size(), outcome.getResourcesInstalled().toString());
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
/**
|
||||
* Copied from https://stackoverflow.com/questions/23003542/cleanest-way-to-create-a-guava-multimap-from-a-java-8-stream
|
||||
*/
|
||||
public class MultimapCollector<T, K, V> implements
|
||||
Collector<T, ListMultimap<K, V>, ListMultimap<K, V>> {
|
||||
|
||||
private final Function<T, K> keyGetter;
|
||||
private final Function<T, V> valueGetter;
|
||||
|
||||
public MultimapCollector(Function<T, K> keyGetter, Function<T, V> valueGetter) {
|
||||
this.keyGetter = keyGetter;
|
||||
this.valueGetter = valueGetter;
|
||||
}
|
||||
|
||||
public static <T, K, V> MultimapCollector<T, K, V> toMultimap(Function<T, K> keyGetter, Function<T, V> valueGetter) {
|
||||
return new MultimapCollector<>(keyGetter, valueGetter);
|
||||
}
|
||||
|
||||
public static <T, K, V> MultimapCollector<T, K, T> toMultimap(Function<T, K> keyGetter) {
|
||||
return new MultimapCollector<>(keyGetter, v -> v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<ListMultimap<K, V>> supplier() {
|
||||
return ArrayListMultimap::create;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<ListMultimap<K, V>, T> accumulator() {
|
||||
return (map, element) -> map.put(keyGetter.apply(element), valueGetter.apply(element));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<ListMultimap<K, V>> combiner() {
|
||||
return (map1, map2) -> {
|
||||
map1.putAll(map2);
|
||||
return map1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<ListMultimap<K, V>, ListMultimap<K, V>> finisher() {
|
||||
return map -> map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Characteristics> characteristics() {
|
||||
return ImmutableSet.of(Characteristics.IDENTITY_FINISH);
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,986 @@
|
|||
{
|
||||
"resourceType": "Bundle",
|
||||
"type": "transaction",
|
||||
"entry": [
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "ExplanationOfBenefit",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Professional-NonClinician"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
|
||||
"code": "payerid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "https://hl7.org/fhir/sid/payerid",
|
||||
"value": "5824473976"
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
|
||||
"code": "uc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "https://hl7.org/fhir/sid/claimid",
|
||||
"value": "1234094"
|
||||
}
|
||||
],
|
||||
"status": "active",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claim-type",
|
||||
"code": "professional"
|
||||
}
|
||||
]
|
||||
},
|
||||
"use": "claim",
|
||||
"patient": {
|
||||
"reference": "Patient/d1a47e2c-509b-e326-deab-597e3f598ca5"
|
||||
},
|
||||
"billablePeriod": {
|
||||
"start": "2017-01-08",
|
||||
"end": "2017-01-08"
|
||||
},
|
||||
"created": "2017-01-11T00:00:00-08:00",
|
||||
"insurer": {
|
||||
"reference": "Organization/5954a17b-0779-334c-4f1c-e894e45d15fb"
|
||||
},
|
||||
"provider": {
|
||||
"reference": "Organization/68ae4f74-afdc-6242-c50e-02ef776d8e5d"
|
||||
},
|
||||
"payee": {
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/payeetype",
|
||||
"code": "provider"
|
||||
}
|
||||
],
|
||||
"text": "Claim paid to VENDOR"
|
||||
},
|
||||
"party": {
|
||||
"reference": "Organization/68ae4f74-afdc-6242-c50e-02ef776d8e5d"
|
||||
}
|
||||
},
|
||||
"outcome": "complete",
|
||||
"disposition": "PAID",
|
||||
"careTeam": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"provider": {
|
||||
"reference": "Practitioner/23eccc61-ab67-bf8a-e464-6260f7989556"
|
||||
},
|
||||
"responsible": false,
|
||||
"role": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/claimcareteamrole",
|
||||
"code": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"provider": {
|
||||
"reference": "Practitioner/dbbc9a06-f685-b481-d739-133755af138e"
|
||||
},
|
||||
"responsible": false,
|
||||
"role": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "referring"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"provider": {
|
||||
"reference": "Practitioner/39b9250c-0d01-cbb0-ea89-0de9c74af511"
|
||||
},
|
||||
"responsible": true,
|
||||
"role": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
|
||||
"code": "performing"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"supportingInfo": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "clmrecvddate"
|
||||
}
|
||||
]
|
||||
},
|
||||
"timingDate": "2017-01-11"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
|
||||
"code": "billingnetworkcontractingstatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"code": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus",
|
||||
"code": "other"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"diagnosis": [
|
||||
{
|
||||
"sequence": 4,
|
||||
"diagnosisCodeableConcept": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/sid/icd-10-cm",
|
||||
"code": "I27.2",
|
||||
"display": "Other secondary pulmonary hypertension"
|
||||
}
|
||||
],
|
||||
"text": "Other secondary pulmonary hypertension"
|
||||
}
|
||||
}
|
||||
],
|
||||
"procedure": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"date": "2017-01-08T00:00:00-08:00",
|
||||
"procedureCodeableConcept": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://www.ama-assn.org/go/cpt",
|
||||
"code": "99233",
|
||||
"display": "Subsequent hospital care for severe problem"
|
||||
}
|
||||
],
|
||||
"text": "SBSQ HOSPITAL CARE/DAY 35 MINUTES"
|
||||
}
|
||||
}
|
||||
],
|
||||
"insurance": [
|
||||
{
|
||||
"focal": true,
|
||||
"coverage": {
|
||||
"reference": "urn:uuid:175dbf4a-7ee2-446d-9938-82eea27871a7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"diagnosisSequence": [
|
||||
4
|
||||
],
|
||||
"procedureSequence": [
|
||||
1
|
||||
],
|
||||
"productOrService": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://www.ama-assn.org/go/cpt",
|
||||
"code": "99233",
|
||||
"display": "Subsequent hospital care for severe problem"
|
||||
}
|
||||
],
|
||||
"text": "SBSQ HOSPITAL CARE/DAY 35 MINUTES"
|
||||
},
|
||||
"servicedPeriod": {
|
||||
"start": "2017-01-08",
|
||||
"end": "2017-01-08"
|
||||
},
|
||||
"locationCodeableConcept": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "https://www.cms.gov/Medicare/Coding/place-of-service-codes/Place_of_Service_Code_Set",
|
||||
"code": "99"
|
||||
}
|
||||
]
|
||||
},
|
||||
"quantity": {
|
||||
"value": 1,
|
||||
"unit": "Units",
|
||||
"system": "http://unitsofmeasure.org",
|
||||
"code": "[arb'U]"
|
||||
},
|
||||
"net": {
|
||||
"value": 317.00,
|
||||
"currency": "USD"
|
||||
},
|
||||
"adjudication": [
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "submitted"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 317.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "benefit"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "copay"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "deductible"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "coinsurance"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "memberliability"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "noncovered"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "priorpayerpaid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "paidtoprovider"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus",
|
||||
"code": "outofnetwork"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"total": [
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "submitted"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 317.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "benefit"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "copay"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
|
||||
"code": "deductible"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "coinsurance"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "memberliability"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 124.69,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "noncovered"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "priorpayerpaid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication",
|
||||
"code": "paidtoprovider"
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
],
|
||||
"payment": {
|
||||
"date": "2017-02-02",
|
||||
"amount": {
|
||||
"value": 0.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "ExplanationOfBenefit?identifier=5824473976"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Patient",
|
||||
"id": "d1a47e2c-509b-e326-deab-597e3f598ca5",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "MR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "https://example.org/front-door",
|
||||
"value": "412563524-CO"
|
||||
}
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"use": "usual",
|
||||
"text": "HYOHWAN MGUIRRE",
|
||||
"family": "MGUIRRE",
|
||||
"given": [
|
||||
"HYOHWAN"
|
||||
]
|
||||
}
|
||||
],
|
||||
"telecom": [
|
||||
{
|
||||
"system": "phone",
|
||||
"value": "719-654-0220",
|
||||
"use": "home"
|
||||
}
|
||||
],
|
||||
"gender": "unknown",
|
||||
"birthDate": "1958-05-12",
|
||||
"address": [
|
||||
{
|
||||
"use": "home",
|
||||
"type": "postal",
|
||||
"line": [
|
||||
"20360 East 45Th Court",
|
||||
"PO Box 523"
|
||||
],
|
||||
"city": "COLORADO SPRINGS",
|
||||
"postalCode": "80922-4166"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Patient/d1a47e2c-509b-e326-deab-597e3f598ca5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fullUrl": "urn:uuid:175dbf4a-7ee2-446d-9938-82eea27871a7",
|
||||
"resource": {
|
||||
"resourceType": "Coverage",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "FILL"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "https://hl7.org/fhir/sid/coverageid",
|
||||
"value": "412563524-CO-80001"
|
||||
}
|
||||
],
|
||||
"status": "active",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
||||
"code": "HMO",
|
||||
"display": "health maintenance organization policy"
|
||||
}
|
||||
],
|
||||
"text": "HMO - HMO COMMERCIAL-HDHP-Signature"
|
||||
},
|
||||
"subscriberId": "412563524",
|
||||
"beneficiary": {
|
||||
"reference": "Patient/d1a47e2c-509b-e326-deab-597e3f598ca5"
|
||||
},
|
||||
"relationship": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship",
|
||||
"code": "self",
|
||||
"display": "Self"
|
||||
}
|
||||
],
|
||||
"text": "The Beneficiary is the Subscriber"
|
||||
},
|
||||
"period": {
|
||||
"start": "2016-01-01",
|
||||
"end": "2017-07-01"
|
||||
},
|
||||
"payor": [
|
||||
{
|
||||
"reference": "Organization/5954a17b-0779-334c-4f1c-e894e45d15fb"
|
||||
}
|
||||
],
|
||||
"class": [
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/coverage-class",
|
||||
"code": "group",
|
||||
"display": "Group"
|
||||
}
|
||||
],
|
||||
"text": "An employee group"
|
||||
},
|
||||
"value": "80001",
|
||||
"name": "CS BRZ HDHP 5500/30%/0 ONX S-NON-MEDICARE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Coverage?identifier=412563524-CO-80001"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Organization",
|
||||
"id": "68ae4f74-afdc-6242-c50e-02ef776d8e5d",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
|
||||
"code": "npi"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "http://hl7.org/fhir/sid/us-npi",
|
||||
"value": "1407833767"
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "TAX"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "urn:oid:2.16.840.1.113883.4.4"
|
||||
}
|
||||
],
|
||||
"active": true,
|
||||
"type": [
|
||||
{
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
|
||||
"code": "prov"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "PIKES PEAK NEPHROLOGY ASSOCIATES PC",
|
||||
"address": [
|
||||
{
|
||||
"use": "work",
|
||||
"type": "physical",
|
||||
"line": [
|
||||
"1914 LELARAY STREET"
|
||||
],
|
||||
"city": "COLORADO SPRINGS",
|
||||
"postalCode": "80909",
|
||||
"country": "USA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Organization/68ae4f74-afdc-6242-c50e-02ef776d8e5d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Organization",
|
||||
"id": "5954a17b-0779-334c-4f1c-e894e45d15fb",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "FILL"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "https://hl7.org/fhir/sid/organizationid",
|
||||
"value": "NATLTAP CO-KFHP-PAY-CO"
|
||||
}
|
||||
],
|
||||
"active": true,
|
||||
"type": [
|
||||
{
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
|
||||
"code": "pay",
|
||||
"display": "Payer"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "KAISER FOUNDATION HEALTHPLAN, INC",
|
||||
"telecom": [
|
||||
{
|
||||
"system": "phone",
|
||||
"value": "1-800-382-4661",
|
||||
"use": "work"
|
||||
}
|
||||
],
|
||||
"address": [
|
||||
{
|
||||
"use": "work",
|
||||
"type": "postal",
|
||||
"line": [
|
||||
"NATIONAL CLAIMS ADMINISTRATION COLORADO",
|
||||
"PO Box 629028"
|
||||
],
|
||||
"city": "El Dorado Hills",
|
||||
"state": "CA",
|
||||
"postalCode": "95762-9028"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Organization/5954a17b-0779-334c-4f1c-e894e45d15fb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Practitioner",
|
||||
"id": "23eccc61-ab67-bf8a-e464-6260f7989556",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "NPI"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "http://hl7.org/fhir/sid/us-npi",
|
||||
"value": "1497983654"
|
||||
}
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"use": "usual",
|
||||
"text": "CASSIDY, HEATHER M (MD)",
|
||||
"family": "CASSIDY",
|
||||
"given": [
|
||||
"HEATHER"
|
||||
],
|
||||
"suffix": [
|
||||
"MD"
|
||||
]
|
||||
}
|
||||
],
|
||||
"address": [
|
||||
{
|
||||
"use": "work",
|
||||
"line": [
|
||||
"Briargate",
|
||||
"1405 Briargate Pkwy #141"
|
||||
],
|
||||
"city": "Colorado Springs",
|
||||
"postalCode": "80920"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Practitioner/23eccc61-ab67-bf8a-e464-6260f7989556"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Practitioner",
|
||||
"id": "dbbc9a06-f685-b481-d739-133755af138e",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "NPI"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "http://hl7.org/fhir/sid/us-npi",
|
||||
"value": "1568467280"
|
||||
},
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "TAX"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "urn:oid:2.16.840.1.113883.4.4",
|
||||
"value": "311669909"
|
||||
}
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"use": "usual",
|
||||
"text": "MOHNSSEN, STEVEN R (MD)",
|
||||
"family": "MOHNSSEN",
|
||||
"given": [
|
||||
"STEVEN"
|
||||
],
|
||||
"suffix": [
|
||||
"MD"
|
||||
]
|
||||
}
|
||||
],
|
||||
"address": [
|
||||
{
|
||||
"use": "work",
|
||||
"line": [
|
||||
"1725 E Boulder St",
|
||||
"Ste 204"
|
||||
],
|
||||
"city": "Colorado Springs",
|
||||
"postalCode": "80909"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Practitioner/dbbc9a06-f685-b481-d739-133755af138e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"resourceType": "Practitioner",
|
||||
"id": "39b9250c-0d01-cbb0-ea89-0de9c74af511",
|
||||
"meta": {
|
||||
"lastUpdated": "2021-06-30",
|
||||
"profile": [
|
||||
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner"
|
||||
]
|
||||
},
|
||||
"identifier": [
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "NPI"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "http://hl7.org/fhir/sid/us-npi",
|
||||
"value": "1679605265"
|
||||
},
|
||||
{
|
||||
"use": "usual",
|
||||
"type": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
"code": "TAX"
|
||||
}
|
||||
]
|
||||
},
|
||||
"system": "urn:oid:2.16.840.1.113883.4.4",
|
||||
"value": "840629252"
|
||||
}
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"use": "usual",
|
||||
"text": "ROSS, MICHAEL D (MD)",
|
||||
"family": "ROSS",
|
||||
"given": [
|
||||
"MICHAEL"
|
||||
],
|
||||
"suffix": [
|
||||
"MD"
|
||||
]
|
||||
}
|
||||
],
|
||||
"address": [
|
||||
{
|
||||
"use": "work",
|
||||
"line": [
|
||||
"1914 Lelaray St"
|
||||
],
|
||||
"city": "Colorado Springs",
|
||||
"postalCode": "80909"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "Practitioner/39b9250c-0d01-cbb0-ea89-0de9c74af511"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -550,7 +550,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"system": "https://healthy.kaiserpermanente.org/front-door",
|
||||
"system": "https://example.org/front-door",
|
||||
"value": "1000116-GA"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -550,7 +550,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"system": "https://healthy.kaiserpermanente.org/front-door",
|
||||
"system": "https://example.org/front-door",
|
||||
"value": "1000116-GA"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -5,7 +5,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
|||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
@ -49,7 +49,7 @@ public class PartitionHelper implements BeforeEachCallback, AfterEachCallback {
|
|||
private boolean myCalled = false;
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
|
||||
RequestPartitionId partitionIdentifyRead(ServletRequestDetails theRequestDetails) {
|
||||
RequestPartitionId partitionIdentifyRead(RequestDetails theRequestDetails) {
|
||||
myCalled = true;
|
||||
if (theRequestDetails == null) {
|
||||
ourLog.info("useful breakpoint :-)");
|
||||
|
@ -67,7 +67,7 @@ public class PartitionHelper implements BeforeEachCallback, AfterEachCallback {
|
|||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
RequestPartitionId partitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
|
||||
RequestPartitionId partitionIdentifyCreate(RequestDetails theRequestDetails) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
|
||||
Logger ourLog = LoggerFactory.getLogger(CqlMeasureEvaluationDstu3Test.class);
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CqlMeasureEvaluationDstu3Test.class);
|
||||
|
||||
@Autowired
|
||||
MeasureOperationsProvider myMeasureOperationsProvider;
|
||||
|
@ -60,7 +60,7 @@ public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
|
|||
String periodStart = this.getPeriodStart(expected);
|
||||
String periodEnd = this.getPeriodEnd(expected);
|
||||
|
||||
this.ourLog.info("Measure: %s, Patient: %s, Start: %s, End: %s", measureId, patientId, periodStart, periodEnd);
|
||||
ourLog.info("Measure: {}, Patient: {}, Start: {}, End: {}", measureId, patientId, periodStart, periodEnd);
|
||||
|
||||
MeasureReport actual = this.myMeasureOperationsProvider.evaluateMeasure(new IdType("Measure", measureId),
|
||||
periodStart, periodEnd, null,
|
||||
|
|
|
@ -38,6 +38,7 @@ public class SearchRuntimeDetails {
|
|||
private String myQueryString;
|
||||
private SearchStatusEnum mySearchStatus;
|
||||
private int myFoundIndexMatchesCount;
|
||||
|
||||
public SearchRuntimeDetails(RequestDetails theRequestDetails, String theSearchUuid) {
|
||||
myRequestDetails = theRequestDetails;
|
||||
mySearchUuid = theSearchUuid;
|
||||
|
|
Loading…
Reference in New Issue