Index canonical references (#1728)

* Index canonical references

* Add changelog

* Fix compile error

* Add whitespace
This commit is contained in:
James Agnew 2020-02-25 21:45:46 -05:00 committed by GitHub
parent b2b0ff22b4
commit cb9906580e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 39 deletions

View File

@ -0,0 +1,5 @@
---
type: add
issue: 1728
title: "Fields of type `canonical` were not previously indexed by the JPA server, meaning that some
default search parameters could not be honoured (e.g. StructureDefinition:valueset). This is now corrected."

View File

@ -3,7 +3,14 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.*; 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.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.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
@ -33,7 +40,11 @@ import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType;
import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
import org.junit.*; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
@ -44,12 +55,30 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; 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 java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*; 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.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@SuppressWarnings({"unchecked", "Duplicates"}) @SuppressWarnings({"unchecked", "Duplicates"})
@ -73,6 +102,28 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setReuseCachedSearchResultsForMillis(null);
} }
@Test
public void testCanonicalReference() {
StructureDefinition sd = new StructureDefinition();
sd.getSnapshot().addElement().getBinding().setValueSet("http://foo");
String id = myStructureDefinitionDao.create(sd).getId().toUnqualifiedVersionless().getValue();
{
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(StructureDefinition.SP_VALUESET, new ReferenceParam("http://foo"));
List<String> ids = toUnqualifiedVersionlessIdValues(myStructureDefinitionDao.search(map));
assertThat(ids, contains(id));
}
{
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(StructureDefinition.SP_VALUESET, new ReferenceParam("http://foo2"));
List<String> ids = toUnqualifiedVersionlessIdValues(myStructureDefinitionDao.search(map));
assertThat(ids, empty());
}
}
@Test @Test
public void testHasConditionAgeCompare() { public void testHasConditionAgeCompare() {
Patient patient = new Patient(); Patient patient = new Patient();
@ -4365,7 +4416,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
private String toStringMultiline(List<?> theResults) { private String toStringMultiline(List<?> theResults) {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
for (Object next : theResults) { for (Object next : theResults) {

View File

@ -84,22 +84,6 @@ public class ResourceLink extends BaseResourceIndex {
super(); super();
} }
public ResourceLink(String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) {
super();
setSourcePath(theSourcePath);
setSourceResource(theSourceResource);
setTargetResourceUrl(theTargetResourceUrl);
setUpdated(theUpdated);
}
public ResourceLink(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource, Date theUpdated) {
super();
setSourcePath(theSourcePath);
setSourceResource(theSourceResource);
setTargetResource(theTargetResource);
setUpdated(theUpdated);
}
@Override @Override
public boolean equals(Object theObj) { public boolean equals(Object theObj) {
if (this == theObj) { if (this == theObj) {
@ -170,6 +154,13 @@ public class ResourceLink extends BaseResourceIndex {
myTargetResourceUrl = theTargetResourceUrl.getValue(); myTargetResourceUrl = theTargetResourceUrl.getValue();
} }
public void setTargetResourceUrlCanonical(String theTargetResourceUrl) {
Validate.notBlank(theTargetResourceUrl);
myTargetResourceType = "(unknown)";
myTargetResourceUrl = theTargetResourceUrl;
}
public Date getUpdated() { public Date getUpdated() {
return myUpdated; return myUpdated;
} }
@ -216,4 +207,34 @@ public class ResourceLink extends BaseResourceIndex {
return b.toString(); return b.toString();
} }
public static ResourceLink forAbsoluteReference(String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) {
ResourceLink retVal = new ResourceLink();
retVal.setSourcePath(theSourcePath);
retVal.setSourceResource(theSourceResource);
retVal.setTargetResourceUrl(theTargetResourceUrl);
retVal.setUpdated(theUpdated);
return retVal;
}
/**
* Factory for canonical URL
*/
public static ResourceLink forLogicalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceUrl, Date theUpdated) {
ResourceLink retVal = new ResourceLink();
retVal.setSourcePath(theSourcePath);
retVal.setSourceResource(theSourceResource);
retVal.setTargetResourceUrlCanonical(theTargetResourceUrl);
retVal.setUpdated(theUpdated);
return retVal;
}
public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource, Date theUpdated) {
ResourceLink retVal = new ResourceLink();
retVal.setSourcePath(theSourcePath);
retVal.setSourceResource(theSourceResource);
retVal.setTargetResource(theTargetResource);
retVal.setUpdated(theUpdated);
return retVal;
}
} }

View File

@ -125,23 +125,29 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
case "uri": case "uri":
case "canonical": case "canonical":
String typeName = toTypeName(value); String typeName = toTypeName(value);
// Canonical has a root type of "uri"
if ("canonical".equals(typeName)) {
IPrimitiveType<?> valuePrimitive = (IPrimitiveType<?>) value; IPrimitiveType<?> valuePrimitive = (IPrimitiveType<?>) value;
IBaseReference fakeReference = (IBaseReference) myContext.getElementDefinition("Reference").newInstance(); IBaseReference fakeReference = (IBaseReference) myContext.getElementDefinition("Reference").newInstance();
fakeReference.setReference(valuePrimitive.getValueAsString()); fakeReference.setReference(valuePrimitive.getValueAsString());
// Canonical has a root type of "uri"
if ("canonical".equals(typeName)) {
/* /*
* See #1583 * See #1583
* Technically canonical fields should not allow local references (e.g. * Technically canonical fields should not allow local references (e.g.
* Questionnaire/123) but it seems reasonable for us to interpret a canonical * Questionnaire/123) but it seems reasonable for us to interpret a canonical
* containing a local reference for what it is, and allow people to seaerch * containing a local reference for what it is, and allow people to search
* based on that. * based on that.
*/ */
IIdType parsed = fakeReference.getReferenceElement(); IIdType parsed = fakeReference.getReferenceElement();
if (parsed.hasIdPart() && parsed.hasResourceType() && !parsed.isAbsolute()) { if (parsed.hasIdPart() && parsed.hasResourceType() && !parsed.isAbsolute()) {
PathAndRef ref = new PathAndRef(searchParam.getName(), path, fakeReference); PathAndRef ref = new PathAndRef(searchParam.getName(), path, fakeReference, false);
params.add(ref);
break;
}
if (parsed.isAbsolute()) {
PathAndRef ref = new PathAndRef(searchParam.getName(), path, fakeReference, true);
params.add(ref); params.add(ref);
break; break;
} }
@ -165,7 +171,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
return; return;
} }
PathAndRef ref = new PathAndRef(searchParam.getName(), path, valueRef); PathAndRef ref = new PathAndRef(searchParam.getName(), path, valueRef, false);
params.add(ref); params.add(ref);
break; break;
default: default:

View File

@ -27,15 +27,21 @@ public class PathAndRef {
private final String myPath; private final String myPath;
private final IBaseReference myRef; private final IBaseReference myRef;
private final String mySearchParamName; private final String mySearchParamName;
private final boolean myCanonical;
/** /**
* Constructor * Constructor
*/ */
public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef) { public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef, boolean theCanonical) {
super(); super();
mySearchParamName = theSearchParamName; mySearchParamName = theSearchParamName;
myPath = thePath; myPath = thePath;
myRef = theRef; myRef = theRef;
myCanonical = theCanonical;
}
public boolean isCanonical() {
return myCanonical;
} }
public String getSearchParamName() { public String getSearchParamName() {

View File

@ -87,8 +87,10 @@ public class ResourceLinkExtractor {
theParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName()); theParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());
if (LogicalReferenceHelper.isLogicalReference(myModelConfig, nextId)) { boolean canonical = thePathAndRef.isCanonical();
ResourceLink resourceLink = new ResourceLink(thePathAndRef.getPath(), theEntity, nextId, theUpdateTime); if (LogicalReferenceHelper.isLogicalReference(myModelConfig, nextId) || canonical) {
String value = nextId.getValue();
ResourceLink resourceLink = ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, theUpdateTime);
if (theParams.myLinks.add(resourceLink)) { if (theParams.myLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId); ourLog.debug("Indexing remote resource reference URL: {}", nextId);
} }
@ -130,7 +132,7 @@ public class ResourceLinkExtractor {
String msg = myContext.getLocalizer().getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue()); String msg = myContext.getLocalizer().getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
throw new InvalidRequestException(msg); throw new InvalidRequestException(msg);
} else { } else {
ResourceLink resourceLink = new ResourceLink(thePathAndRef.getPath(), theEntity, nextId, theUpdateTime); ResourceLink resourceLink = ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theParams.myLinks.add(resourceLink)) { if (theParams.myLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId); ourLog.debug("Indexing remote resource reference URL: {}", nextId);
} }
@ -165,7 +167,7 @@ public class ResourceLinkExtractor {
return null; return null;
} }
return new ResourceLink(nextPathAndRef.getPath(), theEntity, targetResource, theUpdateTime); return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResource, theUpdateTime);
} }
} }

View File

@ -32,7 +32,7 @@ public class ResourceIndexedSearchParamsTest {
myTarget.setResourceType("Organization"); myTarget.setResourceType("Organization");
myParams = new ResourceIndexedSearchParams(source); myParams = new ResourceIndexedSearchParams(source);
ResourceLink link = new ResourceLink("organization", source, myTarget, new Date()); ResourceLink link = ResourceLink.forLocalReference("organization", source, myTarget, new Date());
myParams.getResourceLinks().add(link); myParams.getResourceLinks().add(link);
} }