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:
parent
e359b6d823
commit
2c722e64c2
|
@ -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."
|
|
@ -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:*`."
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue