Add support for including qualified star syntax (#2672)

* Add support for including qualified star syntax

* Add changelog

* Add docs

* Build fix
This commit is contained in:
James Agnew 2021-05-23 16:50:37 -04:00 committed by GitHub
parent e359b6d823
commit 2c722e64c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 392 additions and 197 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 2672
title: "A concurrency error was fixed when using client assigned IDs on a highly concurrent server
with resource deletion disabled."

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2672
title: "Support has been added to the JPA server for `_include` and `_revinclude` where the
value is a qualified star, e.g. `_include=Observation:*`."

View File

@ -144,6 +144,13 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<executions>
<execution><id>validate</id><phase>none</phase></execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -42,6 +42,8 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -82,7 +84,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
public class IdHelperService {
private static final String RESOURCE_PID = "RESOURCE_PID";
private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class);
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
@ -142,7 +144,7 @@ public class IdHelperService {
retVal = new ResourcePersistentId(resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId());
} else {
String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theId);
retVal = myMemoryCacheService.get(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> new ResourcePersistentId(resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId()));
retVal = myMemoryCacheService.getThenPutAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> new ResourcePersistentId(resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId()));
}
} else {
@ -252,7 +254,6 @@ public class IdHelperService {
return retVal;
}
public Optional<String> translatePidIdToForcedIdWithCache(ResourcePersistentId theId) {
return myMemoryCacheService.get(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getIdAsLong(), pid -> myForcedIdDao.findByResourcePid(pid).map(t -> t.getForcedId()));
}
@ -334,7 +335,7 @@ public class IdHelperService {
if (!myDaoConfig.isDeleteEnabled()) {
String key = resourceType + "/" + forcedId;
myMemoryCacheService.put(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, key, lookup);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, key, lookup);
}
}
}
@ -378,7 +379,7 @@ public class IdHelperService {
theTarget.add(t);
if (!myDaoConfig.isDeleteEnabled()) {
String nextKey = Long.toString(t.getResourceId());
myMemoryCacheService.put(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, t);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, t);
}
});
@ -386,12 +387,11 @@ public class IdHelperService {
}
/**
*
* Given a set of PIDs, return a set of public FHIR Resource IDs.
* This function will resolve a forced ID if it resolves, and if it fails to resolve to a forced it, will just return the pid
* Example:
* Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows:
*
* <p>
* [1,2,3] -> ["1","pat1","3"]
*
* @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs.
@ -408,6 +408,7 @@ public class IdHelperService {
return resolvedResourceIds;
}
public Map<Long, Optional<String>> translatePidsToForcedIds(Set<Long> thePids) {
Map<Long, Optional<String>> retVal = new HashMap<>(myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, thePids));
@ -423,7 +424,7 @@ public class IdHelperService {
Long nextResourcePid = forcedId.getResourceId();
Optional<String> nextForcedId = Optional.of(forcedId.getForcedId());
retVal.put(nextResourcePid, nextForcedId);
myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId);
}
});
@ -433,7 +434,7 @@ public class IdHelperService {
.collect(Collectors.toList());
for (Long nextResourcePid : remainingPids) {
retVal.put(nextResourcePid, Optional.empty());
myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty());
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty());
}
return retVal;
@ -491,11 +492,11 @@ public class IdHelperService {
*/
public void addResolvedPidToForcedId(ResourcePersistentId theResourcePersistentId, @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, @Nullable String theForcedId) {
if (theForcedId != null) {
myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.of(theForcedId));
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.of(theForcedId));
String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theForcedId);
myMemoryCacheService.put(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theResourcePersistentId);
}else {
myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.empty());
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theResourcePersistentId);
} else {
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.empty());
}
}

View File

@ -1250,9 +1250,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
theSearch.setStatus(SearchStatusEnum.LOADING);
theSearch.setSearchQueryString(theQueryString, theRequestPartitionId);
if (theParams.hasIncludes()) {
for (Include next : theParams.getIncludes()) {
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), false, next.isRecurse()));
}
}
for (Include next : theParams.getRevIncludes()) {
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), true, next.isRecurse()));
}

View File

@ -787,7 +787,18 @@ public class SearchBuilder implements ISearchBuilder {
iter.remove();
}
// Account for _include=*
boolean matchAll = "*".equals(nextInclude.getValue());
// Account for _include=[resourceType]:*
String wantResourceType = null;
if (!matchAll) {
if (nextInclude.getParamName().equals("*")) {
wantResourceType = nextInclude.getParamType();
matchAll = true;
}
}
if (matchAll) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT r.").append(findPidFieldName);
@ -797,11 +808,27 @@ public class SearchBuilder implements ISearchBuilder {
sqlBuilder.append(" FROM ResourceLink r WHERE r.");
sqlBuilder.append(searchPidFieldName);
sqlBuilder.append(" IN (:target_pids)");
// Technically if the request is a qualified star (e.g. _include=Observation:*) we
// should always be checking the source resource type on the resource link. We don't
// actually index that column though by default, so in order to try and be efficient
// we don't actually include it for includes (but we do for revincludes). This is
// because for an include it doesn't really make sense to include a different
// resource type than the one you are searching on.
if (wantResourceType != null && theReverseMode) {
sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
} else {
wantResourceType = null;
}
String sql = sqlBuilder.toString();
List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<ResourcePersistentId> nextPartition : partitions) {
TypedQuery<?> q = theEntityManager.createQuery(sql, Object[].class);
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
if (wantResourceType != null) {
q.setParameter("want_resource_type", wantResourceType);
}
List<?> results = q.getResultList();
for (Object nextRow : results) {
if (nextRow == null) {

View File

@ -28,6 +28,7 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.support.TransactionSynchronization;
@ -94,6 +95,24 @@ public class MemoryCacheService {
return cache.get(theKey, theSupplier);
}
/**
* Fetch an item from the cache if it exists, and use the loading function to
* obtain it otherwise.
*
* This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}.
*/
public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
assert theCache.myKeyType.isAssignableFrom(theKey.getClass());
Cache<K, T> cache = getCache(theCache);
T retVal = cache.getIfPresent(theKey);
if (retVal == null) {
retVal = theSupplier.apply(theKey);
putAfterCommit(theCache, theKey, retVal);
}
return retVal;
}
public <K, V> V getIfPresent(CacheEnum theCache, K theKey) {
assert theCache.myKeyType.isAssignableFrom(theKey.getClass());
return (V) getCache(theCache).getIfPresent(theKey);

View File

@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
@ -147,12 +148,12 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
creator.run();
}
runInTransaction(()->{
runInTransaction(() -> {
Map<String, Integer> counts = new TreeMap<>();
myResourceTableDao
.findAll()
.stream()
.forEach(t->{
.forEach(t -> {
counts.putIfAbsent(t.getResourceType(), 0);
int value = counts.get(t.getResourceType());
value++;
@ -166,7 +167,6 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
}
@Test
public void testCreateWithClientAssignedId() {
myInterceptorRegistry.registerInterceptor(myRetryInterceptor);
@ -603,4 +603,63 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
}
@Test
public void testTransactionWithCreateClientAssignedIdAndReferenceToThatId() {
myInterceptorRegistry.registerInterceptor(myRetryInterceptor);
myDaoConfig.setDeleteEnabled(false);
ServletRequestDetails srd = mock(ServletRequestDetails.class);
String value = UserRequestRetryVersionConflictsInterceptor.RETRY + "; " + UserRequestRetryVersionConflictsInterceptor.MAX_RETRIES + "=10";
when(srd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.singletonList(value));
when(srd.getUserData()).thenReturn(new HashMap<>());
when(srd.getServer()).thenReturn(new RestfulServer(myFhirCtx));
when(srd.getInterceptorBroadcaster()).thenReturn(new InterceptorService());
List<Future<?>> futures = new ArrayList<>();
int repetitionCount = 3;
for (int i = 0; i < repetitionCount; i++) {
String patientId = "PATIENT" + i;
Runnable task = () -> {
BundleBuilder bb = new BundleBuilder(myFhirCtx);
Patient p = new Patient();
p.setId(patientId);
p.setActive(true);
bb.addTransactionUpdateEntry(p);
Observation obs = new Observation();
obs.setSubject(new Reference("Patient/" + patientId));
bb.addTransactionCreateEntry(obs);
ourLog.info("Submitting transaction");
mySystemDao.transaction(srd, (Bundle) bb.getBundle());
};
for (int j = 0; j < 5; j++) {
Future<?> future = myExecutor.submit(task);
futures.add(future);
}
}
// Look for failures
for (Future<?> next : futures) {
try {
next.get();
ourLog.info("Future produced success");
} catch (Exception e) {
ourLog.info("Future produced exception: {}", e.toString());
throw new AssertionError("Failed with message: " + e.toString(), e);
}
}
// Make sure we saved the object
for (int i = 0; i < repetitionCount; i++) {
Patient patient = myPatientDao.read(new IdType("Patient/PATIENT0"));
assertEquals(true, patient.getActive());
}
}
}

View File

@ -1,40 +1,60 @@
package ca.uhn.fhir.jpa.dao.r4;
import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE;
import static org.apache.commons.lang3.StringUtils.countMatches;
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.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.HasAndListParam;
import ca.uhn.fhir.rest.param.HasOrListParam;
import ca.uhn.fhir.rest.param.HasParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.param.UriParamQualifierEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.util.HapiExtensions;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
@ -116,61 +136,38 @@ import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.collect.Lists;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.HasAndListParam;
import ca.uhn.fhir.rest.param.HasOrListParam;
import ca.uhn.fhir.rest.param.HasParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.param.UriParamQualifierEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE;
import static org.apache.commons.lang3.StringUtils.countMatches;
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.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@SuppressWarnings({"unchecked", "Duplicates"})
public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
@ -607,8 +604,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
String yesterday = new DateType(DateUtils.addDays(new Date(), -1)).getValueAsString();
String tomorrow = new DateType(DateUtils.addDays(new Date(), 1)).getValueAsString();
runInTransaction(()->{
ourLog.info("Resources:\n * {}", myResourceTableDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
runInTransaction(() -> {
ourLog.info("Resources:\n * {}", myResourceTableDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
RuntimeResourceDefinition resDef = myFhirCtx.getResourceDefinition("DiagnosticReport");
@ -768,8 +765,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
pat2.getManagingOrganization().setReferenceElement(orgId);
IIdType patId2 = myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
runInTransaction(()->{
ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
runInTransaction(() -> {
ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
// All patient IDs
@ -1509,7 +1506,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
// Should not crash
myServiceRequestDao.create(serviceRequest);
runInTransaction(()->{
runInTransaction(() -> {
assertEquals(1, myResourceIndexedSearchParamDateDao.findAll().size());
});
}
@ -1527,7 +1524,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
// Should not crash
myServiceRequestDao.create(serviceRequest);
runInTransaction(()->{
runInTransaction(() -> {
assertEquals(1, myResourceIndexedSearchParamDateDao.findAll().size());
});
}
@ -1840,6 +1837,72 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
}
@Test
public void testSearchWithIncludeStarQualified() {
Patient pt = new Patient();
pt.setActive(true);
IIdType ptId = myPatientDao.create(pt, mySrd).getId().toUnqualifiedVersionless();
Encounter enc = new Encounter();
enc.setStatus(Encounter.EncounterStatus.ARRIVED);
IIdType encId = myEncounterDao.create(enc, mySrd).getId().toUnqualifiedVersionless();
Observation obs = new Observation();
obs.getSubject().setReference(ptId.getValue());
obs.getEncounter().setReference(encId.getValue());
IIdType obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
// Async Search
SearchParameterMap map = new SearchParameterMap();
map.addInclude(new Include("Observation:*"));
List<IIdType> ids = toUnqualifiedVersionlessIds(myObservationDao.search(map));
assertThat(ids, containsInAnyOrder(obsId, ptId, encId));
// Sync Search
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.addInclude(new Include("Observation:*"));
ids = toUnqualifiedVersionlessIds(myObservationDao.search(map));
assertThat(ids, containsInAnyOrder(obsId, ptId, encId));
}
@Test
public void testSearchWithRevIncludeStarQualified() {
Patient pt = new Patient();
pt.setActive(true);
IIdType ptId = myPatientDao.create(pt, mySrd).getId().toUnqualifiedVersionless();
Encounter enc = new Encounter();
enc.setStatus(Encounter.EncounterStatus.ARRIVED);
IIdType encId = myEncounterDao.create(enc, mySrd).getId().toUnqualifiedVersionless();
Observation obs = new Observation();
obs.getSubject().setReference(ptId.getValue());
obs.getEncounter().setReference(encId.getValue());
IIdType obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
MedicationRequest mr = new MedicationRequest();
mr.getEncounter().setReference(encId.getValue());
myMedicationRequestDao.create(mr, mySrd);
// Async Search
SearchParameterMap map = new SearchParameterMap();
map.addRevInclude(new Include("Observation:*"));
List<IIdType> ids = toUnqualifiedVersionlessIds(myEncounterDao.search(map));
assertThat(ids, containsInAnyOrder(obsId, encId));
// Sync Search
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.addRevInclude(new Include("Observation:*"));
ids = toUnqualifiedVersionlessIds(myEncounterDao.search(map));
assertThat(ids, containsInAnyOrder(obsId, encId));
}
@Test
public void testComponentQuantity() {
Observation o1 = new Observation();
@ -3902,8 +3965,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
runInTransaction(()->{
ourLog.info("Token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().filter(t->t.getParamName().equals("identifier")).map(t->t.toString()).collect(Collectors.joining("\n * ")));
runInTransaction(() -> {
ourLog.info("Token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().filter(t -> t.getParamName().equals("identifier")).map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
{
@ -3945,8 +4008,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
female = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue();
}
runInTransaction(()->{
ourLog.info("Tokens:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
runInTransaction(() -> {
ourLog.info("Tokens:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
List<String> patients;
@ -5446,9 +5509,9 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
c3.getEncounter().setReference(e3Id);
myCommunicationDao.create(c3);
runInTransaction(()->{
ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
ourLog.info("Dates:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
runInTransaction(() -> {
ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
ourLog.info("Dates:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
SearchParameterMap map;

View File

@ -58,13 +58,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class SearchParameterMap implements Serializable {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class);
public static final Integer INTEGER_0 = 0;
private final HashMap<String, List<List<IQueryParameterType>>> mySearchParameterMap = new LinkedHashMap<>();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class);
private static final long serialVersionUID = 1L;
private final HashMap<String, List<List<IQueryParameterType>>> mySearchParameterMap = new LinkedHashMap<>();
private Integer myCount;
private Integer myOffset;
private EverythingModeEnum myEverythingMode = null;
@ -467,7 +464,9 @@ public class SearchParameterMap implements Serializable {
sort = sort.getChain();
}
if (hasIncludes()) {
addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes());
}
addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes());
if (getLastUpdated() != null) {
@ -514,6 +513,13 @@ public class SearchParameterMap implements Serializable {
return b.toString();
}
/**
* @since 5.5.0
*/
public boolean hasIncludes() {
return myIncludes != null && !myIncludes.isEmpty();
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
@ -555,14 +561,14 @@ public class SearchParameterMap implements Serializable {
theAndOrParams.removeIf(List::isEmpty);
}
public void setNearDistanceParam(QuantityParam theQuantityParam) {
myNearDistanceParam = theQuantityParam;
}
public QuantityParam getNearDistanceParam() {
return myNearDistanceParam;
}
public void setNearDistanceParam(QuantityParam theQuantityParam) {
myNearDistanceParam = theQuantityParam;
}
public boolean isWantOnlyCount() {
return SummaryEnum.COUNT.equals(getSummaryMode()) || INTEGER_0.equals(getCount());
}
@ -576,6 +582,52 @@ public class SearchParameterMap implements Serializable {
return this;
}
public List<List<IQueryParameterType>> get(String theName) {
return mySearchParameterMap.get(theName);
}
public void put(String theName, List<List<IQueryParameterType>> theParams) {
mySearchParameterMap.put(theName, theParams);
}
public boolean containsKey(String theName) {
return mySearchParameterMap.containsKey(theName);
}
public Set<String> keySet() {
return mySearchParameterMap.keySet();
}
public boolean isEmpty() {
return mySearchParameterMap.isEmpty();
}
// Wrapper methods
public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() {
return mySearchParameterMap.entrySet();
}
public List<List<IQueryParameterType>> remove(String theName) {
return mySearchParameterMap.remove(theName);
}
public int size() {
return mySearchParameterMap.size();
}
public SearchContainedModeEnum getSearchContainedMode() {
return mySearchContainedMode;
}
public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) {
if (theSearchContainedMode == null) {
mySearchContainedMode = SearchContainedModeEnum.FALSE;
} else {
this.mySearchContainedMode = theSearchContainedMode;
}
}
public enum EverythingModeEnum {
/*
* Don't reorder! We rely on the ordinals
@ -689,40 +741,6 @@ public class SearchParameterMap implements Serializable {
return retVal;
}
// Wrapper methods
public List<List<IQueryParameterType>> get(String theName) {
return mySearchParameterMap.get(theName);
}
public void put(String theName, List<List<IQueryParameterType>> theParams) {
mySearchParameterMap.put(theName, theParams);
}
public boolean containsKey(String theName) {
return mySearchParameterMap.containsKey(theName);
}
public Set<String> keySet() {
return mySearchParameterMap.keySet();
}
public boolean isEmpty() {
return mySearchParameterMap.isEmpty();
}
public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() {
return mySearchParameterMap.entrySet();
}
public List<List<IQueryParameterType>> remove(String theName) {
return mySearchParameterMap.remove(theName);
}
public int size() {
return mySearchParameterMap.size();
}
public static SearchParameterMap newSynchronous() {
SearchParameterMap retVal = new SearchParameterMap();
retVal.setLoadSynchronous(true);
@ -736,17 +754,5 @@ public class SearchParameterMap implements Serializable {
return retVal;
}
public SearchContainedModeEnum getSearchContainedMode() {
return mySearchContainedMode;
}
public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) {
if (theSearchContainedMode == null) {
mySearchContainedMode = SearchContainedModeEnum.FALSE;
} else {
this.mySearchContainedMode = theSearchContainedMode;
}
}
}

View File

@ -2204,7 +2204,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.41.1</version>
<version>8.42</version>
</dependency>
</dependencies>
<configuration>