Handle Versioned Canonical Refs (#6003)

* Work on versioned canonidal refs

* Merge

* Tests working

* Working without version support

* CLean refactor

* Targets

* Remove redundant parameter

* Fix up tests

* Remove fixme

* Add changelog

* Improve changelog

* Update version

* Test fixes
This commit is contained in:
James Agnew 2024-06-17 08:14:24 -04:00 committed by GitHub
parent 72b50a4e6d
commit 8947706af3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 775 additions and 331 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,12 @@
---
type: add
issue: 6003
title: "When performing a search for a resource which uses versioned canonical
references to another resource (e.g. a QuestionnaireResponse with a
questionnaire reference of `http://example.org/my-questionnaire|1.0`),
the server failed to process `_include` directives which should bring in
these references. Includes will now fetch all versions of a canonical
reference, which is still not perfect but is an improvement over the
current behaviour. A future update may add more targeted behaviour to
address including versioned reference targets, but this will require a
significant re-architecture of the loading module."

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -42,6 +42,7 @@ public class IResourceTableDaoImpl implements IForcedIdQueries {
* is an object array, where the order matters (the array represents columns returned by the query).
* Deleted resources are not filtered.
*/
@Override
public Collection<Object[]> findAndResolveByForcedIdWithNoTypeIncludeDeleted(
String theResourceType, Collection<String> theForcedIds) {
return findAndResolveByForcedIdWithNoType(theResourceType, theForcedIds, false);
@ -52,6 +53,7 @@ public class IResourceTableDaoImpl implements IForcedIdQueries {
* is an object array, where the order matters (the array represents columns returned by the query).
* Deleted resources are optionally filtered. Be careful if you change this query in any way.
*/
@Override
public Collection<Object[]> findAndResolveByForcedIdWithNoType(
String theResourceType, Collection<String> theForcedIds, boolean theExcludeDeleted) {
String query = "SELECT t.myResourceType, t.myId, t.myFhirId, t.myDeleted "
@ -74,6 +76,7 @@ public class IResourceTableDaoImpl implements IForcedIdQueries {
* is an object array, where the order matters (the array represents columns returned by the query).
* Deleted resources are optionally filtered. Be careful if you change this query in any way.
*/
@Override
public Collection<Object[]> findAndResolveByForcedIdWithNoTypeInPartition(
String theResourceType,
Collection<String> theForcedIds,
@ -100,6 +103,7 @@ public class IResourceTableDaoImpl implements IForcedIdQueries {
* is an object array, where the order matters (the array represents columns returned by the query).
* Deleted resources are optionally filtered. Be careful if you change this query in any way.
*/
@Override
public Collection<Object[]> findAndResolveByForcedIdWithNoTypeInPartitionNull(
String theResourceType, Collection<String> theForcedIds, boolean theExcludeDeleted) {
String query = "SELECT t.myResourceType, t.myId, t.myFhirId, t.myDeleted "
@ -122,6 +126,7 @@ public class IResourceTableDaoImpl implements IForcedIdQueries {
* is an object array, where the order matters (the array represents columns returned by the query).
* Deleted resources are optionally filtered. Be careful if you change this query in any way.
*/
@Override
public Collection<Object[]> findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(
String theResourceType,
Collection<String> theForcedIds,

View File

@ -19,6 +19,8 @@
*/
package ca.uhn.fhir.jpa.search.builder;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ComboSearchParamType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
@ -163,7 +165,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
public static boolean myUseMaxPageSize50ForTest = false;
protected final IInterceptorBroadcaster myInterceptorBroadcaster;
protected final IResourceTagDao myResourceTagDao;
private final String myResourceName;
String myResourceName;
private final Class<? extends IBaseResource> myResourceType;
private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
private final SqlObjectFactory mySqlBuilderFactory;
@ -195,6 +197,9 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
@Autowired(required = false)
private IElasticsearchSvc myIElasticsearchSvc;
@Autowired
private FhirContext myCtx;
@Autowired
private IJpaStorageResourceParser myJpaStorageResourceParser;
@ -1309,7 +1314,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, theParameters.getRequestDetails())) {
CurrentThreadCaptureQueriesListener.startCapturing();
}
if (matches.size() == 0) {
if (matches.isEmpty()) {
return new HashSet<>();
}
if (currentIncludes == null || currentIncludes.isEmpty()) {
@ -1356,196 +1361,32 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
}
if (matchAll) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT r.").append(findPidFieldName);
sqlBuilder.append(", r.").append(findResourceTypeFieldName);
if (findVersionFieldName != null) {
sqlBuilder.append(", r.").append(findVersionFieldName);
}
sqlBuilder.append(" FROM ResourceLink r WHERE ");
sqlBuilder.append("r.");
sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id
sqlBuilder.append(" IN (:target_pids)");
/*
* We need to set the resource type in 2 cases only:
* 1) we are in $everything mode
* (where we only want to fetch specific resource types, regardless of what is
* available to fetch)
* 2) we are doing revincludes
*
* 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
&& (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
// because mySourceResourceType is not part of the HFJ_RES_LINK
// index, this might not be the most optimal performance.
// but it is for an $everything operation (and maybe we should update the index)
sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
} else {
wantResourceType = null;
}
// When calling $everything on a Patient instance, we don't want to recurse into new Patient
// resources
// (e.g. via Provenance, List, or Group) when in an $everything operation
if (myParams != null
&& myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream()
.collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')")));
}
if (hasDesiredResourceTypes) {
sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
}
String sql = sqlBuilder.toString();
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<JpaPid> nextPartition : partitions) {
TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
if (wantResourceType != null) {
q.setParameter("want_resource_type", wantResourceType);
}
if (maxCount != null) {
q.setMaxResults(maxCount);
}
if (hasDesiredResourceTypes) {
q.setParameter("desired_target_resource_types", desiredResourceTypes);
}
List<?> results = q.getResultList();
for (Object nextRow : results) {
if (nextRow == null) {
// This can happen if there are outgoing references which are canonical or point to
// other servers
continue;
}
Long version = null;
Long resourceLink = (Long) ((Object[]) nextRow)[0];
String resourceType = (String) ((Object[]) nextRow)[1];
if (findVersionFieldName != null) {
version = (Long) ((Object[]) nextRow)[2];
}
if (resourceLink != null) {
JpaPid pid =
JpaPid.fromIdAndVersionAndResourceType(resourceLink, version, resourceType);
pidsToInclude.add(pid);
}
}
}
loadIncludesMatchAll(
findPidFieldName,
findResourceTypeFieldName,
findVersionFieldName,
searchPidFieldName,
wantResourceType,
reverseMode,
hasDesiredResourceTypes,
nextRoundMatches,
entityManager,
maxCount,
desiredResourceTypes,
pidsToInclude,
request);
} else {
List<String> paths;
// Start replace
RuntimeSearchParam param;
String resType = nextInclude.getParamType();
if (isBlank(resType)) {
continue;
}
RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
if (def == null) {
ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
continue;
}
String paramName = nextInclude.getParamName();
if (isNotBlank(paramName)) {
param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
} else {
param = null;
}
if (param == null) {
ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
continue;
}
paths = param.getPathsSplitForResourceType(resType);
// end replace
Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
for (String nextPath : paths) {
String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID)
? "src_resource_id"
: "target_resource_id";
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
if (findVersionFieldName != null) {
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
}
// Query for includes lookup has 2 cases
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
// url in target_resource_url
// Case 1:
Map<String, Object> localReferenceQueryParams = new HashMap<>();
String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID)
? "target_resource_id"
: "src_resource_id";
StringBuilder localReferenceQuery =
new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r "
+ " WHERE r.src_path = :src_path AND "
+ " r.target_resource_id IS NOT NULL AND "
+ " r."
+ searchPidFieldSqlColumn + " IN (:target_pids) ");
localReferenceQueryParams.put("src_path", nextPath);
// we loop over target_pids later.
if (targetResourceTypes != null) {
if (targetResourceTypes.size() == 1) {
localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
localReferenceQueryParams.put(
"target_resource_type",
targetResourceTypes.iterator().next());
} else {
localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
}
}
// Case 2:
Pair<String, Map<String, Object>> canonicalQuery = buildCanonicalUrlQuery(
findVersionFieldName, searchPidFieldSqlColumn, targetResourceTypes);
// @formatter:on
String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<JpaPid> nextPartition : partitions) {
Query q = entityManager.createNativeQuery(sql, Tuple.class);
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
localReferenceQueryParams.forEach(q::setParameter);
canonicalQuery.getRight().forEach(q::setParameter);
if (maxCount != null) {
q.setMaxResults(maxCount);
}
@SuppressWarnings("unchecked")
List<Tuple> results = q.getResultList();
for (Tuple result : results) {
if (result != null) {
Long resourceId =
NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
Long resourceVersion = null;
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
resourceVersion = NumberUtils.createLong(
String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
}
pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion));
}
}
}
}
loadIncludesMatchSpecific(
nextInclude,
fhirContext,
findPidFieldName,
findVersionFieldName,
searchPidFieldName,
reverseMode,
nextRoundMatches,
entityManager,
maxCount,
pidsToInclude);
}
}
@ -1609,9 +1450,264 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
return allAdded;
}
private void loadIncludesMatchSpecific(
Include nextInclude,
FhirContext fhirContext,
String findPidFieldName,
String findVersionFieldName,
String searchPidFieldName,
boolean reverseMode,
List<JpaPid> nextRoundMatches,
EntityManager entityManager,
Integer maxCount,
HashSet<JpaPid> pidsToInclude) {
List<String> paths;
// Start replace
RuntimeSearchParam param;
String resType = nextInclude.getParamType();
if (isBlank(resType)) {
return;
}
RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
if (def == null) {
ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
return;
}
String paramName = nextInclude.getParamName();
if (isNotBlank(paramName)) {
param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
} else {
param = null;
}
if (param == null) {
ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
return;
}
paths = param.getPathsSplitForResourceType(resType);
// end replace
Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
for (String nextPath : paths) {
String findPidFieldSqlColumn =
findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id";
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
if (findVersionFieldName != null) {
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
}
// Query for includes lookup has 2 cases
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
// url in target_resource_url
// Case 1:
Map<String, Object> localReferenceQueryParams = new HashMap<>();
String searchPidFieldSqlColumn =
searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id";
StringBuilder localReferenceQuery = new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r "
+ " WHERE r.src_path = :src_path AND "
+ " r.target_resource_id IS NOT NULL AND "
+ " r."
+ searchPidFieldSqlColumn + " IN (:target_pids) ");
localReferenceQueryParams.put("src_path", nextPath);
// we loop over target_pids later.
if (targetResourceTypes != null) {
if (targetResourceTypes.size() == 1) {
localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
localReferenceQueryParams.put(
"target_resource_type",
targetResourceTypes.iterator().next());
} else {
localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
}
}
// Case 2:
Pair<String, Map<String, Object>> canonicalQuery =
buildCanonicalUrlQuery(findVersionFieldName, targetResourceTypes, reverseMode);
String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<JpaPid> nextPartition : partitions) {
Query q = entityManager.createNativeQuery(sql, Tuple.class);
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
localReferenceQueryParams.forEach(q::setParameter);
canonicalQuery.getRight().forEach(q::setParameter);
if (maxCount != null) {
q.setMaxResults(maxCount);
}
@SuppressWarnings("unchecked")
List<Tuple> results = q.getResultList();
for (Tuple result : results) {
if (result != null) {
Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
Long resourceVersion = null;
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
resourceVersion =
NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
}
pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion));
}
}
}
}
}
private void loadIncludesMatchAll(
String findPidFieldName,
String findResourceTypeFieldName,
String findVersionFieldName,
String searchPidFieldName,
String wantResourceType,
boolean reverseMode,
boolean hasDesiredResourceTypes,
List<JpaPid> nextRoundMatches,
EntityManager entityManager,
Integer maxCount,
List<String> desiredResourceTypes,
HashSet<JpaPid> pidsToInclude,
RequestDetails request) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT r.").append(findPidFieldName);
sqlBuilder.append(", r.").append(findResourceTypeFieldName);
sqlBuilder.append(", r.myTargetResourceUrl");
if (findVersionFieldName != null) {
sqlBuilder.append(", r.").append(findVersionFieldName);
}
sqlBuilder.append(" FROM ResourceLink r WHERE ");
sqlBuilder.append("r.");
sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id
sqlBuilder.append(" IN (:target_pids)");
/*
* We need to set the resource type in 2 cases only:
* 1) we are in $everything mode
* (where we only want to fetch specific resource types, regardless of what is
* available to fetch)
* 2) we are doing revincludes
*
* 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 && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
// because mySourceResourceType is not part of the HFJ_RES_LINK
// index, this might not be the most optimal performance.
// but it is for an $everything operation (and maybe we should update the index)
sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
} else {
wantResourceType = null;
}
// When calling $everything on a Patient instance, we don't want to recurse into new Patient
// resources
// (e.g. via Provenance, List, or Group) when in an $everything operation
if (myParams != null
&& myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream()
.collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')")));
}
if (hasDesiredResourceTypes) {
sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
}
String sql = sqlBuilder.toString();
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<JpaPid> nextPartition : partitions) {
TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
if (wantResourceType != null) {
q.setParameter("want_resource_type", wantResourceType);
}
if (maxCount != null) {
q.setMaxResults(maxCount);
}
if (hasDesiredResourceTypes) {
q.setParameter("desired_target_resource_types", desiredResourceTypes);
}
List<?> results = q.getResultList();
Set<String> canonicalUrls = null;
for (Object nextRow : results) {
if (nextRow == null) {
// This can happen if there are outgoing references which are canonical or point to
// other servers
continue;
}
Long version = null;
Long resourceId = (Long) ((Object[]) nextRow)[0];
String resourceType = (String) ((Object[]) nextRow)[1];
String resourceCanonicalUrl = (String) ((Object[]) nextRow)[2];
if (findVersionFieldName != null) {
version = (Long) ((Object[]) nextRow)[3];
}
if (resourceId != null) {
JpaPid pid = JpaPid.fromIdAndVersionAndResourceType(resourceId, version, resourceType);
pidsToInclude.add(pid);
} else if (resourceCanonicalUrl != null) {
if (canonicalUrls == null) {
canonicalUrls = new HashSet<>();
}
canonicalUrls.add(resourceCanonicalUrl);
}
}
if (canonicalUrls != null) {
String message =
"Search with _include=* can be inefficient when references using canonical URLs are detected. Use more specific _include values instead.";
firePerformanceWarning(request, message);
loadCanonicalUrls(canonicalUrls, entityManager, pidsToInclude, reverseMode);
}
}
}
private void loadCanonicalUrls(
Set<String> theCanonicalUrls,
EntityManager theEntityManager,
HashSet<JpaPid> thePidsToInclude,
boolean theReverse) {
StringBuilder sqlBuilder;
Set<Long> identityHashesForTypes = calculateIndexUriIdentityHashesForResourceTypes(null, theReverse);
List<Collection<String>> canonicalUrlPartitions =
partition(theCanonicalUrls, getMaximumPageSize() - identityHashesForTypes.size());
sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT i.myResourcePid ");
sqlBuilder.append("FROM ResourceIndexedSearchParamUri i ");
sqlBuilder.append("WHERE i.myHashIdentity IN (:hash_identity) ");
sqlBuilder.append("AND i.myUri IN (:uris)");
String canonicalResSql = sqlBuilder.toString();
for (Collection<String> nextCanonicalUrlList : canonicalUrlPartitions) {
TypedQuery<Long> canonicalResIdQuery = theEntityManager.createQuery(canonicalResSql, Long.class);
canonicalResIdQuery.setParameter("hash_identity", identityHashesForTypes);
canonicalResIdQuery.setParameter("uris", nextCanonicalUrlList);
List<Long> resIds = canonicalResIdQuery.getResultList();
for (var next : resIds) {
if (next != null) {
thePidsToInclude.add(JpaPid.fromId(next));
}
}
}
}
/**
* Given a
* @param request
* Sends a raw SQL query to the Pointcut for raw SQL queries.
*/
private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) {
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
@ -1641,30 +1737,22 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
@Nonnull
private Pair<String, Map<String, Object>> buildCanonicalUrlQuery(
String theVersionFieldName, String thePidFieldSqlColumn, Set<String> theTargetResourceTypes) {
String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
String theVersionFieldName, Set<String> theTargetResourceTypes, boolean theReverse) {
String fieldsToLoadFromSpidxUriTable = theReverse ? "r.src_resource_id" : "rUri.res_id";
if (theVersionFieldName != null) {
// canonical-uri references aren't versioned, but we need to match the column count for the UNION
fieldsToLoadFromSpidxUriTable += ", NULL";
}
// The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url.
// But sp_name isn't indexed, so we use hash_identity instead.
if (theTargetResourceTypes == null) {
// hash_identity includes the resource type. So a null wildcard must be replaced with a list of all types.
theTargetResourceTypes = myDaoRegistry.getRegisteredDaoTypes();
}
assert !theTargetResourceTypes.isEmpty();
Set<Long> identityHashesForTypes = theTargetResourceTypes.stream()
.map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity(
myPartitionSettings, myRequestPartitionId, type, "url"))
.collect(Collectors.toSet());
Set<Long> identityHashesForTypes =
calculateIndexUriIdentityHashesForResourceTypes(theTargetResourceTypes, theReverse);
Map<String, Object> canonicalUriQueryParams = new HashMap<>();
StringBuilder canonicalUrlQuery = new StringBuilder(
"SELECT " + fieldsToLoadFromSpidxUriTable + " FROM hfj_res_link r " + " JOIN hfj_spidx_uri rUri ON ( ");
// join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2
if (theTargetResourceTypes.size() == 1) {
if (theTargetResourceTypes != null && theTargetResourceTypes.size() == 1) {
canonicalUrlQuery.append(" rUri.hash_identity = :uri_identity_hash ");
canonicalUriQueryParams.put(
"uri_identity_hash", identityHashesForTypes.iterator().next());
@ -1673,21 +1761,102 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
canonicalUriQueryParams.put("uri_identity_hashes", identityHashesForTypes);
}
canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )" + " WHERE r.src_path = :src_path AND "
+ " r.target_resource_id IS NULL AND "
+ " r."
+ thePidFieldSqlColumn + " IN (:target_pids) ");
canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )");
canonicalUrlQuery.append(" WHERE r.src_path = :src_path AND ");
canonicalUrlQuery.append(" r.target_resource_id IS NULL ");
canonicalUrlQuery.append(" AND ");
if (theReverse) {
canonicalUrlQuery.append("rUri.res_id");
} else {
canonicalUrlQuery.append("r.src_resource_id");
}
canonicalUrlQuery.append(" IN (:target_pids) ");
return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams);
}
private List<Collection<JpaPid>> partition(Collection<JpaPid> theNextRoundMatches, int theMaxLoad) {
@Nonnull
Set<Long> calculateIndexUriIdentityHashesForResourceTypes(Set<String> theTargetResourceTypes, boolean theReverse) {
Set<String> targetResourceTypes = theTargetResourceTypes;
if (targetResourceTypes == null) {
/*
* If we don't have a list of valid target types, we need to figure out a list of all
* possible target types in order to perform the search of the URI index table. This is
* because the hash_identity column encodes the resource type, so we'll need a hash
* value for each possible target type.
*/
targetResourceTypes = new HashSet<>();
Set<String> possibleTypes = myDaoRegistry.getRegisteredDaoTypes();
if (theReverse) {
// For reverse includes, it is really hard to figure out what types
// are actually potentially pointing to the type we're searching for
// in this context, so let's just assume it could be anything.
targetResourceTypes = possibleTypes;
} else {
for (var next : mySearchParamRegistry.getActiveSearchParams(myResourceName).values().stream()
.filter(t -> t.getParamType().equals(RestSearchParameterTypeEnum.REFERENCE))
.collect(Collectors.toList())) {
// If the reference points to a Reference (ie not a canonical or CanonicalReference)
// then it doesn't matter here anyhow. The logic here only works for elements at the
// root level of the document (e.g. QuestionnaireResponse.subject or
// QuestionnaireResponse.subject.where(...)) but this is just an optimization
// anyhow.
if (next.getPath().startsWith(myResourceName + ".")) {
String elementName =
next.getPath().substring(next.getPath().indexOf('.') + 1);
int secondDotIndex = elementName.indexOf('.');
if (secondDotIndex != -1) {
elementName = elementName.substring(0, secondDotIndex);
}
BaseRuntimeChildDefinition child =
myContext.getResourceDefinition(myResourceName).getChildByName(elementName);
if (child != null) {
BaseRuntimeElementDefinition<?> childDef = child.getChildByName(elementName);
if (childDef != null) {
if (childDef.getName().equals("Reference")) {
continue;
}
}
}
}
if (!next.getTargets().isEmpty()) {
// For each reference parameter on the resource type we're searching for,
// add all the potential target types to the list of possible target
// resource types we can look up.
for (var nextTarget : next.getTargets()) {
if (possibleTypes.contains(nextTarget)) {
targetResourceTypes.add(nextTarget);
}
}
} else {
// If we have any references that don't define any target types, then
// we need to assume that all enabled resource types are possible target
// types
targetResourceTypes.addAll(possibleTypes);
break;
}
}
}
}
assert !targetResourceTypes.isEmpty();
Set<Long> identityHashesForTypes = targetResourceTypes.stream()
.map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity(
myPartitionSettings, myRequestPartitionId, type, "url"))
.collect(Collectors.toSet());
return identityHashesForTypes;
}
private <T> List<Collection<T>> partition(Collection<T> theNextRoundMatches, int theMaxLoad) {
if (theNextRoundMatches.size() <= theMaxLoad) {
return Collections.singletonList(theNextRoundMatches);
} else {
List<Collection<JpaPid>> retVal = new ArrayList<>();
Collection<JpaPid> current = null;
for (JpaPid next : theNextRoundMatches) {
List<Collection<T>> retVal = new ArrayList<>();
Collection<T> current = null;
for (T next : theNextRoundMatches) {
if (current == null) {
current = new ArrayList<>(theMaxLoad);
retVal.add(current);
@ -2121,19 +2290,11 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
}
private void sendProcessingMsgAndFirePerformanceHook() {
StorageProcessingMessage message = new StorageProcessingMessage();
String msg = "Pass completed with no matching results seeking rows "
+ myPidSet.size() + "-" + mySkipCount
+ ". This indicates an inefficient query! Retrying with new max count of "
+ myMaxResultsToFetch;
ourLog.warn(msg);
message.setMessage(msg);
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(StorageProcessingMessage.class, message);
CompositeInterceptorBroadcaster.doCallHooks(
myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
firePerformanceWarning(myRequest, msg);
}
private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) {
@ -2208,6 +2369,18 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
}
}
private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
ourLog.warn(theMessage);
StorageProcessingMessage message = new StorageProcessingMessage();
message.setMessage(theMessage);
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, message);
CompositeInterceptorBroadcaster.doCallHooks(
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
}
public static int getMaximumPageSize() {
if (myUseMaxPageSize50ForTest) {
return MAXIMUM_PAGE_SIZE_FOR_TESTING;

View File

@ -0,0 +1,70 @@
package ca.uhn.fhir.jpa.search.builder;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SearchBuilderTest {
public static final FhirContext ourCtx = FhirContext.forR4Cached();
@Spy
private FhirContext myFhirContext = ourCtx;
@Spy
private ISearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(myFhirContext);
@Spy
private PartitionSettings myPartitionSettings = new PartitionSettings();
@Mock(strictness = Mock.Strictness.LENIENT)
private DaoRegistry myDaoRegistry;
@InjectMocks
private SearchBuilder mySearchBuilder;
@BeforeEach
public void beforeEach() {
mySearchBuilder.myResourceName = "QuestionnaireResponse";
when(myDaoRegistry.getRegisteredDaoTypes()).thenReturn(ourCtx.getResourceTypes());
}
@Test
void testCalculateIndexUriIdentityHashesForResourceTypes_Include_Null() {
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(null, false);
// There are only 12 resource types that actually can be linked to by the QuestionnaireResponse
// resource via canonical references in any parameters
assertThat(types).hasSize(1);
}
@Test
void testCalculateIndexUriIdentityHashesForResourceTypes_Include_Nonnull() {
Set<String> inputTypes = Set.of("Questionnaire");
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(inputTypes, false);
// Just the one that we actually specified
assertThat(types).hasSize(1);
}
@Test
void testCalculateIndexUriIdentityHashesForResourceTypes_RevInclude_Null() {
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(null, true);
// Revincludes are really hard to figure out the potential resource types for, so we just need to
// use all active resource types
assertThat(types).hasSize(146);
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -228,6 +228,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
b.append("paramName", getParamName());
b.append("uri", myUri);
b.append("hashUri", myHashUri);
b.append("hashIdentity", myHashIdentity);
return b.toString();
}

View File

@ -304,11 +304,11 @@ public class ResourceLink extends BaseResourceIndex {
StringBuilder b = new StringBuilder();
b.append("ResourceLink[");
b.append("path=").append(mySourcePath);
b.append(", src=").append(mySourceResourcePid);
b.append(", target=").append(myTargetResourcePid);
b.append(", targetType=").append(myTargetResourceType);
b.append(", targetVersion=").append(myTargetResourceVersion);
b.append(", targetUrl=").append(myTargetResourceUrl);
b.append(", srcResId=").append(mySourceResourcePid);
b.append(", targetResId=").append(myTargetResourcePid);
b.append(", targetResType=").append(myTargetResourceType);
b.append(", targetResVersion=").append(myTargetResourceVersion);
b.append(", targetResUrl=").append(myTargetResourceUrl);
b.append("]");
return b.toString();

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -2161,6 +2161,12 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
if (parsed.isAbsolute()) {
String refValue =
fakeReference.getReferenceElement().getValue();
if (refValue.contains("|")) {
fakeReference.setReference(refValue.substring(0, refValue.indexOf('|')));
}
myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, fakeReference, true);
theParams.add(myPathAndRef);
break;

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.ReindexParameters;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
@ -72,6 +73,8 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Provenance;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.StringType;
@ -1505,6 +1508,40 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
@Test
public void testSearchWithRevInclude() {
Questionnaire q = new Questionnaire();
q.setId("q");
q.setUrl("http://foo");
q.setVersion("1.0");
myQuestionnaireDao.update(q, mySrd);
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setId("qr");
qr.setQuestionnaire("http://foo");
myQuestionnaireResponseDao.update(qr, mySrd);
logAllResourceLinks();
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("_id", new ReferenceParam("Questionnaire/q"));
map.addRevInclude(QuestionnaireResponse.INCLUDE_QUESTIONNAIRE);
IFhirResourceDao<?> dao = myQuestionnaireDao;
dao.search(map, mySrd);
myCaptureQueriesListener.clear();
IBundleProvider outcome = dao.search(map, mySrd);
toUnqualifiedVersionlessIdValues(outcome);
assertEquals(4, myCaptureQueriesListener.countSelectQueries());
myCaptureQueriesListener.clear();
outcome = dao.search(map, mySrd);
toUnqualifiedVersionlessIdValues(outcome);
assertEquals(4, myCaptureQueriesListener.countSelectQueries());
}
/**
* See the class javadoc before changing the counts in this test!
*/

View File

@ -1,10 +1,15 @@
package ca.uhn.fhir.jpa.dao.r4;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
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.DateParam;
@ -12,9 +17,7 @@ import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BodyStructure;
import org.hl7.fhir.r4.model.CarePlan;
@ -23,34 +26,169 @@ import org.hl7.fhir.r4.model.EpisodeOfCare;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.springframework.transaction.support.TransactionTemplate;
import java.sql.Date;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@SuppressWarnings({"unchecked", "Duplicates"})
@SuppressWarnings({"Duplicates"})
public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchIncludeTest.class);
@Mock
private IAnonymousInterceptor myAnonymousInterceptor;
@Captor
private ArgumentCaptor<HookParams> myParamsCaptor;
@AfterEach
public void afterEach() {
myStorageSettings.setMaximumIncludesToLoadPerPage(JpaStorageSettings.DEFAULT_MAXIMUM_INCLUDES_TO_LOAD_PER_PAGE);
myInterceptorRegistry.unregisterInterceptor(myAnonymousInterceptor);
}
@ParameterizedTest
@CsvSource({
// theQuestionnaireRespId, theReverse, theMatchAll
"QuestionnaireResponse/qr , false, false",
"QuestionnaireResponse/qr2 , false, false",
"QuestionnaireResponse/qr , true, false",
"QuestionnaireResponse/qr2 , true, false",
"QuestionnaireResponse/qr , false, true",
"QuestionnaireResponse/qr2 , false, true",
// "QuestionnaireResponse/qr , true, true", // Not yet supported
// "QuestionnaireResponse/qr2 , true, true", // Not yet supported
})
public void testIncludeCanonicalReference(String theQuestionnaireRespId, boolean theReverse, boolean theMatchAll) {
Questionnaire qWrongVersion = new Questionnaire();
qWrongVersion.setId("qWrongVersion");
qWrongVersion.setUrl("http://foo");
qWrongVersion.setVersion("99.0");
myQuestionnaireDao.update(qWrongVersion, mySrd);
Questionnaire q = new Questionnaire();
q.setId("q");
q.setUrl("http://foo");
q.setVersion("1.0");
myQuestionnaireDao.update(q, mySrd);
if (theQuestionnaireRespId.equals("QuestionnaireResponse/qr")) {
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setId("qr");
qr.setQuestionnaire("http://foo");
myQuestionnaireResponseDao.update(qr, mySrd);
} else {
QuestionnaireResponse qr2 = new QuestionnaireResponse();
qr2.setId("qr2");
qr2.setQuestionnaire("http://foo|1.0");
myQuestionnaireResponseDao.update(qr2, mySrd);
}
logAllUriIndexes();
logAllResourceLinks();
// Create a QR and Q that have other URLs and shouldn't be turned up in searches here
Questionnaire qIrrelevant = new Questionnaire();
qIrrelevant.setId("qIrrelevant");
qIrrelevant.setUrl("http://fooIrrelevant");
qIrrelevant.setVersion("1.0");
myQuestionnaireDao.update(qIrrelevant, mySrd);
QuestionnaireResponse qrIrrelevant = new QuestionnaireResponse();
qrIrrelevant.setId("qrIrrelevant");
qrIrrelevant.setQuestionnaire("http://fooIrrelevant");
myQuestionnaireResponseDao.update(qrIrrelevant, mySrd);
IBundleProvider outcome;
IFhirResourceDao<?> dao;
SearchParameterMap map;
String expectWarning = null;
if (theReverse) {
map = new SearchParameterMap();
map.add("_id", new TokenParam("Questionnaire/q"));
if (theMatchAll) {
map.addRevInclude(IBaseResource.INCLUDE_ALL);
} else {
map.addRevInclude(QuestionnaireResponse.INCLUDE_QUESTIONNAIRE);
}
dao = myQuestionnaireDao;
} else {
map = new SearchParameterMap();
map.add("_id", new TokenParam(theQuestionnaireRespId));
if (theMatchAll) {
map.addInclude(IBaseResource.INCLUDE_ALL);
} else {
map.addInclude(QuestionnaireResponse.INCLUDE_QUESTIONNAIRE);
}
dao = myQuestionnaireResponseDao;
}
if (theMatchAll) {
expectWarning = "Search with _include=* can be inefficient";
}
myCaptureQueriesListener.clear();
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.JPA_PERFTRACE_WARNING, myAnonymousInterceptor);
map.setLoadSynchronous(true);
outcome = dao.search(map, mySrd);
List<String> outcomeValues = toUnqualifiedVersionlessIdValues(outcome);
myCaptureQueriesListener.logSelectQueries();
if (theReverse) {
assertThat(outcomeValues).as(outcomeValues.toString()).containsExactlyInAnyOrder(
theQuestionnaireRespId, "Questionnaire/q"
);
} else {
assertThat(outcomeValues).as(outcomeValues.toString()).containsExactlyInAnyOrder(
theQuestionnaireRespId, "Questionnaire/q", "Questionnaire/qWrongVersion"
);
}
if (expectWarning == null) {
verify(myAnonymousInterceptor, never()).invoke(eq(Pointcut.JPA_PERFTRACE_WARNING), myParamsCaptor.capture());
} else {
verify(myAnonymousInterceptor, times(1)).invoke(eq(Pointcut.JPA_PERFTRACE_WARNING), myParamsCaptor.capture());
HookParams params = myParamsCaptor.getValue();
assertThat(params.get(StorageProcessingMessage.class).getMessage()).contains(expectWarning);
}
if (!theReverse && theMatchAll) {
SqlQuery searchForCanonicalReferencesQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(3);
// Make sure we have the right query - If this ever fails, maybe we have optimized the queries
// (or somehow made things worse) and the search for the canonical target is no longer the 4th
// SQL query
assertThat(searchForCanonicalReferencesQuery.getSql(true, false)).contains("rispu1_0.HASH_IDENTITY in ('-600769180185160063')");
assertThat(searchForCanonicalReferencesQuery.getSql(true, false)).contains("rispu1_0.SP_URI in ('http://foo')");
}
}
@Test
public void testIncludesNotAppliedToIncludedResources() {
createOrganizationWithReferencingEpisodesOfCare(10);
@ -59,7 +197,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
.add("_id", new TokenParam("EOC-0"))
.addInclude(new Include("*"))
.addRevInclude(new Include("*").setRecurse(true));
IBundleProvider results = myEpisodeOfCareDao.search(map);
IBundleProvider results = myEpisodeOfCareDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
assertThat(ids).as(ids.toString()).containsExactlyInAnyOrder("EpisodeOfCare/EOC-0", "Organization/ORG-0");
}
@ -72,7 +210,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
.setCount(10)
.addInclude(new Include("*"))
.addRevInclude(new Include("*").setRecurse(true));
IBundleProvider results = myOrganizationDao.search(map);
IBundleProvider results = myOrganizationDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
List<String> expected = IntStream.range(0, 10)
@ -97,7 +235,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
.add("_id", new TokenParam("ORG-0"))
.addInclude(new Include("*"))
.addRevInclude(new Include("*").setRecurse(true));
IBundleProvider results = myOrganizationDao.search(map);
IBundleProvider results = myOrganizationDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
assertThat(ids).as(ids.toString()).containsExactlyInAnyOrder("EpisodeOfCare/EOC-0", "EpisodeOfCare/EOC-1", "EpisodeOfCare/EOC-2", "EpisodeOfCare/EOC-3", "EpisodeOfCare/EOC-4", "Organization/ORG-0");
}
@ -110,7 +248,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
SearchParameterMap map = SearchParameterMap.newSynchronous()
.addInclude(new Include("CarePlan.patient"));
try {
myCarePlanDao.search(map);
myCarePlanDao.search(map, mySrd);
fail();
} catch (Exception e) {
// good
@ -120,7 +258,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
SearchParameterMap map2 = SearchParameterMap.newSynchronous()
.addInclude(new Include("CarePlan:patient"));
try {
IBundleProvider results = myCarePlanDao.search(map2);
IBundleProvider results = myCarePlanDao.search(map2, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
assertThat(ids).as(ids.toString()).containsExactlyInAnyOrder("CarePlan/CP-1", "Patient/PAT-1");
} catch (Exception e) {
@ -161,7 +299,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
logAllResources();
logAllResourceLinks();
// Non synchronous
// Non-synchronous
SearchParameterMap map = new SearchParameterMap();
map.add("_id", new TokenParam("PRA8780542726"));
map.addRevInclude(new Include("Procedure:part-of"));
@ -194,7 +332,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
.setCount(10)
.addInclude(new Include("*"))
.addRevInclude(new Include("*").setRecurse(true));
IBundleProvider results = myOrganizationDao.search(map);
IBundleProvider results = myOrganizationDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
assertThat(ids).as(ids.toString()).containsExactlyInAnyOrder("EpisodeOfCare/EOC-0", "EpisodeOfCare/EOC-1", "EpisodeOfCare/EOC-2", "EpisodeOfCare/EOC-3", "Organization/ORG-0", "Organization/ORG-P");
@ -210,7 +348,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
.add("_id", new TokenParam("ORG-0"))
.addRevInclude(EpisodeOfCare.INCLUDE_ORGANIZATION);
myCaptureQueriesListener.clear();
IBundleProvider results = myOrganizationDao.search(map);
IBundleProvider results = myOrganizationDao.search(map, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(results);
myCaptureQueriesListener.logSelectQueries();
assertThat(ids).as(ids.toString()).containsExactlyInAnyOrder("EpisodeOfCare/EOC-0", "EpisodeOfCare/EOC-1", "EpisodeOfCare/EOC-2", "EpisodeOfCare/EOC-3", "EpisodeOfCare/EOC-4", "EpisodeOfCare/EOC-5", "EpisodeOfCare/EOC-6", "EpisodeOfCare/EOC-7", "EpisodeOfCare/EOC-8", "EpisodeOfCare/EOC-9", "Organization/ORG-0");
@ -223,38 +361,40 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
Organization org = new Organization();
org.setId("Organization/ORG-P");
org.setName("ORG-P");
myOrganizationDao.update(org);
myOrganizationDao.update(org, mySrd);
org = new Organization();
org.setId("Organization/ORG-0");
org.setName("ORG-0");
org.setPartOf(new Reference("Organization/ORG-P"));
myOrganizationDao.update(org);
myOrganizationDao.update(org, mySrd);
for (int i = 0; i < theEocCount; i++) {
EpisodeOfCare eoc = new EpisodeOfCare();
eoc.setId("EpisodeOfCare/EOC-" + i);
eoc.getManagingOrganization().setReference("Organization/ORG-0");
myEpisodeOfCareDao.update(eoc);
myEpisodeOfCareDao.update(eoc, mySrd);
}
}
@SuppressWarnings("SameParameterValue")
private void createPatientWithReferencingCarePlan(int theCount) {
org.hl7.fhir.r4.model.Patient patient = new Patient();
patient.setId("Patient/PAT-1");
myPatientDao.update(patient);
myPatientDao.update(patient, mySrd);
for (int i = 1; i <= theCount; i++) {
CarePlan carePlan = new CarePlan();
carePlan.setId("CarePlan/CP-" + i);
carePlan.getSubject().setReference("Patient/PAT-1");
myCarePlanDao.update(carePlan);
myCarePlanDao.update(carePlan, mySrd);
}
}
/**
* https://github.com/hapifhir/hapi-fhir/issues/4896
* <a href="https://github.com/hapifhir/hapi-fhir/issues/4896">#4896</a>
*/
@SuppressWarnings("DataFlowIssue")
@Test
void testLastUpdatedDoesNotApplyToForwardOrRevIncludes() {
// given
@ -277,7 +417,7 @@ public class FhirResourceDaoR4SearchIncludeTest extends BaseJpaR4Test {
// when
// "Patient?_lastUpdated=gt2023-01-01&_revinclude=Group:member&_revinclude=CareTeam:subject&_include=Patient:organization");
SearchParameterMap map = new SearchParameterMap();
map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, Date.from(now))));
map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, java.util.Date.from(now))));
map.addInclude(new Include("Patient:organization"));
map.addRevInclude(new Include("Group:member"));
map.addRevInclude(new Include("CareTeam:subject"));

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -8,7 +8,7 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<description>An open-source implementation of the FHIR specification in Java.</description>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.6-SNAPSHOT</version>
<version>7.3.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>