Preserve meta values in stored resources (#2481)

* Fix #1731

* Test fix

* Test fixes

* Avoid intermittent failure
This commit is contained in:
James Agnew 2021-03-17 09:23:22 -04:00 committed by GitHub
parent d92a6787c5
commit d857048207
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 199 additions and 14 deletions

View File

@ -131,7 +131,7 @@ public abstract class BaseParser implements IParser {
}
@Override
public IParser setDontEncodeElements(Set<String> theDontEncodeElements) {
public IParser setDontEncodeElements(Collection<String> theDontEncodeElements) {
if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) {
myDontEncodeElements = null;
} else {

View File

@ -249,7 +249,7 @@ public interface IParser {
* @param theDontEncodeElements The elements to encode
* @see #setEncodeElements(Set)
*/
IParser setDontEncodeElements(Set<String> theDontEncodeElements);
IParser setDontEncodeElements(Collection<String> theDontEncodeElements);
/**
* If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this
@ -264,7 +264,7 @@ public interface IParser {
* </ul>
*
* @param theEncodeElements The elements to encode
* @see #setDontEncodeElements(Set)
* @see #setDontEncodeElements(Collection)
*/
IParser setEncodeElements(Set<String> theEncodeElements);

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 1731
title: "When storing resources in the JPA server, extensions in `Resource.meta` were not preserved, nor were
any contents in `Bundle.entry.resource.meta`. Both of these things are now correctly persisted and
returned. Thanks to Sean McIlvenna for reporting!"

View File

@ -51,7 +51,6 @@ import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
@ -96,6 +95,7 @@ import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseReference;
@ -511,11 +511,64 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (thePerformIndexing) {
encoding = myConfig.getResourceEncoding();
Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
String resourceType = theEntity.getResourceType();
List<String> excludeElements = new ArrayList<>(8);
excludeElements.add("id");
IBaseMetaType meta = theResource.getMeta();
boolean hasExtensions = false;
IBaseExtension<?,?> sourceExtension = null;
if (meta instanceof IBaseHasExtensions) {
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) meta).getExtension();
if (!extensions.isEmpty()) {
hasExtensions = true;
/*
* FHIR DSTU3 did not have the Resource.meta.source field, so we use a
* custom HAPI FHIR extension in Resource.meta to store that field. However,
* we put the value for that field in a separate table so we don't want to serialize
* it into the stored BLOB. Therefore: remove it from the resource temporarily
* and restore it afterward.
*/
if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
for (int i = 0; i < extensions.size(); i++) {
if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
sourceExtension = extensions.remove(i);
i--;
}
}
}
}
}
if (hasExtensions) {
excludeElements.add(resourceType + ".meta.profile");
excludeElements.add(resourceType + ".meta.tag");
excludeElements.add(resourceType + ".meta.security");
excludeElements.add(resourceType + ".meta.versionId");
excludeElements.add(resourceType + ".meta.lastUpdated");
excludeElements.add(resourceType + ".meta.source");
} else {
/*
* If there are no extensions in the meta element, we can just exclude the
* whole meta element, which avoids adding an empty "meta":{}
* from showing up in the serialized JSON.
*/
excludeElements.add(resourceType + ".meta");
}
theEntity.setFhirVersion(myContext.getVersion().getVersion());
bytes = encodeResource(theResource, encoding, excludeElements, myContext);
if (sourceExtension != null) {
IBaseExtension<?, ?> newSourceExtension = ((IBaseHasExtensions) meta).addExtension();
newSourceExtension.setUrl(sourceExtension.getUrl());
newSourceExtension.setValue(sourceExtension.getValue());
}
HashFunction sha256 = Hashing.sha256();
String hashSha256 = sha256.hashBytes(bytes).toString();
if (hashSha256.equals(theEntity.getHashSha256()) == false) {
@ -1530,7 +1583,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
return resourceText;
}
public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, Set<String> theExcludeElements, FhirContext theContext) {
public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, List<String> theExcludeElements, FhirContext theContext) {
byte[] bytes;
IParser parser = theEncoding.newParser(theContext);
parser.setDontEncodeElements(theExcludeElements);

View File

@ -0,0 +1,126 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.SampledData;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class FhirResourceDaoR4MetaTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4MetaTest.class);
/**
* See #1731
*/
@Test
public void testMetaExtensionsPreserved() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().addExtension("http://foo", new StringType("hello"));
IIdType id = myPatientDao.create(patient).getId();
patient = myPatientDao.read(id);
assertTrue(patient.getActive());
assertEquals(1, patient.getMeta().getExtensionsByUrl("http://foo").size());
assertEquals("hello", patient.getMeta().getExtensionByUrl("http://foo").getValueAsPrimitive().getValueAsString());
}
/**
* See #1731
*/
@Test
public void testBundleInnerResourceMetaIsPreserved() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().setLastUpdatedElement(new InstantType("2011-01-01T12:12:12Z"));
patient.getMeta().setVersionId("22");
patient.getMeta().addProfile("http://foo");
patient.getMeta().addTag("http://tag", "value", "the tag");
patient.getMeta().addSecurity("http://tag", "security", "the tag");
patient.getMeta().addExtension("http://foo", new StringType("hello"));
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.COLLECTION);
bundle.addEntry().setResource(patient);
IIdType id = myBundleDao.create(bundle).getId();
bundle = myBundleDao.read(id);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
patient = (Patient) bundle.getEntryFirstRep().getResource();
assertTrue(patient.getActive());
assertEquals(1, patient.getMeta().getExtensionsByUrl("http://foo").size());
assertEquals("22", patient.getMeta().getVersionId());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("hello", patient.getMeta().getExtensionByUrl("http://foo").getValueAsPrimitive().getValueAsString());
}
/**
* See #1731
*/
@Test
public void testMetaValuesNotStoredAfterDeletion() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().addProfile("http://foo");
patient.getMeta().addTag("http://tag", "value", "the tag");
patient.getMeta().addSecurity("http://tag", "security", "the tag");
IIdType id = myPatientDao.create(patient).getId();
Meta meta = new Meta();
meta.addProfile("http://foo");
meta.addTag("http://tag", "value", "the tag");
meta.addSecurity("http://tag", "security", "the tag");
myPatientDao.metaDeleteOperation(id, meta, mySrd);
patient = myPatientDao.read(id);
assertThat(patient.getMeta().getProfile(), empty());
assertThat(patient.getMeta().getTag(), empty());
assertThat(patient.getMeta().getSecurity(), empty());
}
}

View File

@ -64,7 +64,9 @@ import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString;
@ -622,6 +624,12 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
// The paging should have ended now - but the last redacted female result is an empty existing page which should never have been there.
assertNotNull(BundleUtil.getLinkUrlOfType(myFhirCtx, response, "next"));
await()
.until(
()->mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException()).getStatus(),
equalTo(SearchStatusEnum.FINISHED)
);
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException());
assertEquals(3, search.getNumFound());

View File

@ -35,9 +35,7 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ResourceMetaParams {
/**
@ -48,7 +46,6 @@ public class ResourceMetaParams {
* These are parameters which are supported by searches
*/
public static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
public static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<>();
@ -67,10 +64,5 @@ public class ResourceMetaParams {
resourceMetaAndParams.put(Constants.PARAM_HAS, HasAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
}
}