Fixes a bug with tags. (#4813)

* Test, fix

* Drop constraint, add migration

* Add changelog

* Fix userSelected null vs false

* Fix merge

* Fix up checkstyle whining

* One more failure

* Fix test

* wip

* changelog clarity

Co-authored-by: James Agnew <jamesagnew@gmail.com>

* change index

---------

Co-authored-by: Michael Buckley <michaelabuckley@gmail.com>
Co-authored-by: James Agnew <jamesagnew@gmail.com>
This commit is contained in:
Tadgh 2023-05-05 15:06:03 -04:00 committed by GitHub
parent c637cec622
commit f0726a32d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 373 additions and 130 deletions

View File

@ -19,13 +19,14 @@
*/
package ca.uhn.fhir.model.api;
import java.net.URI;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import java.net.URI;
import java.util.Objects;
/**
* A single tag
* <p>
@ -58,7 +59,7 @@ public class Tag extends BaseElement implements IElement, IBaseCoding {
private String myScheme;
private String myTerm;
private String myVersion;
private boolean myUserSelected;
private Boolean myUserSelected;
public Tag() {
}
@ -114,37 +115,22 @@ public class Tag extends BaseElement implements IElement, IBaseCoding {
if (getClass() != obj.getClass())
return false;
Tag other = (Tag) obj;
if (myScheme == null) {
if (other.myScheme != null)
return false;
} else if (!myScheme.equals(other.myScheme))
return false;
if (myTerm == null) {
if (other.myTerm != null)
return false;
} else if (!myTerm.equals(other.myTerm))
return false;
if (myVersion == null) {
if (other.getVersion() != null)
return false;
} else if (!myVersion.equals(other.getVersion()))
return false;
if (myUserSelected != other.getUserSelected())
return false;
return true;
return
Objects.equals(myScheme, other.myScheme) &&
Objects.equals(myTerm, other.myTerm) &&
Objects.equals(myVersion, other.myVersion) &&
Objects.equals(myUserSelected, other.myUserSelected);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((myScheme == null) ? 0 : myScheme.hashCode());
result = prime * result + ((myTerm == null) ? 0 : myTerm.hashCode());
result = prime * result + ((myVersion == null) ? 0 : myVersion.hashCode());
result = prime * result + Boolean.hashCode(myUserSelected);
result = prime * result + Objects.hashCode(myScheme);
result = prime * result + Objects.hashCode(myTerm);
result = prime * result + Objects.hashCode(myVersion);
result = prime * result + Objects.hashCode(myUserSelected);
return result;
}
@ -234,7 +220,9 @@ public class Tag extends BaseElement implements IElement, IBaseCoding {
}
@Override
public boolean getUserSelected() { return myUserSelected; }
public boolean getUserSelected() { return myUserSelected != null && myUserSelected; }
public Boolean getUserSelectedBoolean() { return myUserSelected; }
@Override
public IBaseCoding setUserSelected(boolean theUserSelected) {
@ -242,4 +230,8 @@ public class Tag extends BaseElement implements IElement, IBaseCoding {
return this;
}
public void setUserSelectedBoolean(Boolean theUserSelected) {
myUserSelected = theUserSelected;
}
}

View File

@ -724,73 +724,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
if (theResource instanceof IResource) {
IResource resource = (IResource) theResource;
// Object securityLabelRawObj =
List<BaseCodingDt> securityLabels = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.SECURITY_LABELS);
List<? extends IIdType> profiles = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.PROFILES);
profiles = super.getProfileTagsForEncoding(resource, profiles);
TagList tags = getMetaTagsForEncoding(resource, theEncodeContext);
InstantDt updated = (InstantDt) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED);
IdDt resourceId = resource.getId();
String versionIdPart = resourceId.getVersionIdPart();
if (isBlank(versionIdPart)) {
versionIdPart = ResourceMetadataKeyEnum.VERSION.get(resource);
}
List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = getExtensionMetadataKeys(resource);
if (super.shouldEncodeResourceMeta(resource) && (ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) || !extensionMetadataKeys.isEmpty()) {
beginObject(theEventWriter, "meta");
if (shouldEncodePath(resource, "meta.versionId")) {
writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart);
}
if (shouldEncodePath(resource, "meta.lastUpdated")) {
writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated);
}
if (profiles != null && profiles.isEmpty() == false) {
beginArray(theEventWriter, "profile");
for (IIdType profile : profiles) {
if (profile != null && isNotBlank(profile.getValue())) {
theEventWriter.write(profile.getValue());
}
}
theEventWriter.endArray();
}
if (securityLabels.isEmpty() == false) {
beginArray(theEventWriter, "security");
for (BaseCodingDt securityLabel : securityLabels) {
theEventWriter.beginObject();
theEncodeContext.pushPath("security", false);
encodeCompositeElementChildrenToStreamWriter(resDef, resource, securityLabel, theEventWriter, theContainedResource, null, theEncodeContext);
theEncodeContext.popPath();
theEventWriter.endObject();
}
theEventWriter.endArray();
}
if (tags != null && tags.isEmpty() == false) {
beginArray(theEventWriter, "tag");
for (Tag tag : tags) {
if (tag.isEmpty()) {
continue;
}
theEventWriter.beginObject();
writeOptionalTagWithTextNode(theEventWriter, "system", tag.getScheme());
writeOptionalTagWithTextNode(theEventWriter, "code", tag.getTerm());
writeOptionalTagWithTextNode(theEventWriter, "display", tag.getLabel());
theEventWriter.endObject();
}
theEventWriter.endArray();
}
addExtensionMetadata(theResDef, theResource, theContainedResource, extensionMetadataKeys, resDef, theEventWriter, theEncodeContext);
theEventWriter.endObject(); // end meta
}
parseMetaForDSTU2(theResDef, theResource, theEventWriter, theContainedResource, theEncodeContext, resDef);
}
encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef, theEncodeContext), theEncodeContext);
@ -798,6 +732,77 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
theEventWriter.endObject();
}
private void parseMetaForDSTU2(RuntimeResourceDefinition theResDef, IBaseResource theResource, BaseJsonLikeWriter theEventWriter, boolean theContainedResource, EncodeContext theEncodeContext, RuntimeResourceDefinition resDef) throws IOException {
IResource resource = (IResource) theResource;
// Object securityLabelRawObj =
List<BaseCodingDt> securityLabels = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.SECURITY_LABELS);
List<? extends IIdType> profiles = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.PROFILES);
profiles = super.getProfileTagsForEncoding(resource, profiles);
TagList tags = getMetaTagsForEncoding(resource, theEncodeContext);
InstantDt updated = (InstantDt) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED);
IdDt resourceId = resource.getId();
String versionIdPart = resourceId.getVersionIdPart();
if (isBlank(versionIdPart)) {
versionIdPart = ResourceMetadataKeyEnum.VERSION.get(resource);
}
List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = getExtensionMetadataKeys(resource);
if (super.shouldEncodeResourceMeta(resource) && (ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) || !extensionMetadataKeys.isEmpty()) {
beginObject(theEventWriter, "meta");
if (shouldEncodePath(resource, "meta.versionId")) {
writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart);
}
if (shouldEncodePath(resource, "meta.lastUpdated")) {
writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated);
}
if (profiles != null && profiles.isEmpty() == false) {
beginArray(theEventWriter, "profile");
for (IIdType profile : profiles) {
if (profile != null && isNotBlank(profile.getValue())) {
theEventWriter.write(profile.getValue());
}
}
theEventWriter.endArray();
}
if (securityLabels.isEmpty() == false) {
beginArray(theEventWriter, "security");
for (BaseCodingDt securityLabel : securityLabels) {
theEventWriter.beginObject();
theEncodeContext.pushPath("security", false);
encodeCompositeElementChildrenToStreamWriter(resDef, resource, securityLabel, theEventWriter, theContainedResource, null, theEncodeContext);
theEncodeContext.popPath();
theEventWriter.endObject();
}
theEventWriter.endArray();
}
if (tags != null && tags.isEmpty() == false) {
beginArray(theEventWriter, "tag");
for (Tag tag : tags) {
if (tag.isEmpty()) {
continue;
}
theEventWriter.beginObject();
writeOptionalTagWithTextNode(theEventWriter, "system", tag.getScheme());
writeOptionalTagWithTextNode(theEventWriter, "code", tag.getTerm());
writeOptionalTagWithTextNode(theEventWriter, "display", tag.getLabel());
// wipmb should we be writing the new properties here? There must be another path.
theEventWriter.endObject();
}
theEventWriter.endArray();
}
addExtensionMetadata(theResDef, theResource, theContainedResource, extensionMetadataKeys, resDef, theEventWriter, theEncodeContext);
theEventWriter.endObject(); // end meta
}
}
private void addExtensionMetadata(RuntimeResourceDefinition theResDef, IBaseResource theResource,
boolean theContainedResource,

View File

@ -27,7 +27,7 @@ public class TagTest {
@Test
public void testHashCode() {
Tag tag1 = new Tag().setScheme("scheme").setTerm("term").setLabel("label");
assertEquals(-1029266947, tag1.hashCode());
assertEquals(-1029268184, tag1.hashCode());
}
@Test

View File

@ -364,7 +364,9 @@ public class VersionCanonicalizer {
retVal.setSystem(coding.getSystem());
retVal.setDisplay(coding.getDisplay());
retVal.setVersion(coding.getVersion());
retVal.setUserSelected(!coding.getUserSelectedElement().isEmpty() && coding.getUserSelected());
if (!coding.getUserSelectedElement().isEmpty()) {
retVal.setUserSelected( coding.getUserSelected() );
}
return retVal;
}

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 4819
title: "Tags no longer provide a default `false` value for `userSelected`."

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 4813
title: "Under heavy concurrency, a bug resulted in identical tag definitions being rejected with a `NonUniqueResultException` some of the time. This has been corrected."

View File

@ -247,6 +247,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
@Autowired
private PlatformTransactionManager myTransactionManager;
protected final CodingSpy myCodingSpy = new CodingSpy();
@VisibleForTesting
public void setExternallyStoredResourceServiceRegistryForUnitTest(ExternallyStoredResourceServiceRegistry theExternallyStoredResourceServiceRegistry) {
myExternallyStoredResourceServiceRegistry = theExternallyStoredResourceServiceRegistry;
@ -283,7 +285,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (tagList != null) {
for (Tag next : tagList) {
TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.TAG, next.getScheme(), next.getTerm(),
next.getLabel(), next.getVersion(), next.getUserSelected());
next.getLabel(), next.getVersion(), myCodingSpy.getBooleanObject(next));
if (def != null) {
ResourceTag tag = theEntity.addTag(def);
allDefs.add(tag);
@ -323,7 +325,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (tagList != null) {
for (IBaseCoding next : tagList) {
TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.TAG, next.getSystem(), next.getCode(),
next.getDisplay(), next.getVersion(), next.getUserSelected());
next.getDisplay(), next.getVersion(), myCodingSpy.getBooleanObject(next));
if (def != null) {
ResourceTag tag = theEntity.addTag(def);
theAllTags.add(tag);
@ -335,7 +337,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
if (securityLabels != null) {
for (IBaseCoding next : securityLabels) {
TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay(), null, null);
TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion(), myCodingSpy.getBooleanObject(next));
if (def != null) {
ResourceTag tag = theEntity.addTag(def);
theAllTags.add(tag);
@ -394,7 +396,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
MemoryCacheService.TagDefinitionCacheKey key = toTagDefinitionMemoryCacheKey(theTagType, theScheme, theTerm, theVersion, theUserSelected);
TagDefinition retVal = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.TAG_DEFINITION, key);
if (retVal == null) {
HashMap<MemoryCacheService.TagDefinitionCacheKey, TagDefinition> resolvedTagDefinitions = theTransactionDetails
.getOrCreateUserData(HapiTransactionService.XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS, HashMap::new);
@ -425,6 +426,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
String theVersion, Boolean theUserSelected) {
TypedQuery<TagDefinition> q = buildTagQuery(theTagType, theScheme, theTerm, theVersion, theUserSelected);
q.setMaxResults(1);
TransactionTemplate template = new TransactionTemplate(myTransactionManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
@ -525,9 +527,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
? builder.isNull(from.get("myVersion"))
: builder.equal(from.get("myVersion"), theVersion));
predicates.add( isNull(theUserSelected) || isFalse(theUserSelected)
? builder.isFalse(from.get("myUserSelected"))
: builder.isTrue(from.get("myUserSelected")));
predicates.add( isNull(theUserSelected)
? builder.isNull(from.get("myUserSelected"))
: builder.equal(from.get("myUserSelected"), theUserSelected));
cq.where(predicates.toArray(new Predicate[0]));
return myEntityManager.createQuery(cq);
@ -817,12 +819,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
// Update the resource to contain the old tags
allTagsOld.forEach(tag -> {
theResource.getMeta()
IBaseCoding iBaseCoding = theResource.getMeta()
.addTag()
.setCode(tag.getTag().getCode())
.setSystem(tag.getTag().getSystem())
.setVersion(tag.getTag().getVersion())
.setUserSelected(tag.getTag().getUserSelected());
.setVersion(tag.getTag().getVersion());
if (tag.getTag().getUserSelected() != null) {
iBaseCoding.setUserSelected(tag.getTag().getUserSelected());
}
});
theEntity.setHasTags(!allTagsNew.isEmpty());

View File

@ -834,6 +834,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
if (!hasTag) {
theEntity.setHasTags(true);

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.primitive.BooleanDt;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* We are trying to preserve null behaviour despite IBaseCoding using primitive boolean for userSelected.
*/
public class CodingSpy {
final Map<Class, Field> mySpies = new HashMap<>();
/**
* Reach into the Coding and pull out the Boolean instead of the boolean.
* @param theValue the Coding, or CodingDt
* @return the Boolean
*/
public Boolean getBooleanObject(IBaseCoding theValue) {
Field spy = getSpy(theValue);
try {
Object o = spy.get(theValue);
if (o == null) {
return null;
}
if (o instanceof BooleanDt) {
BooleanDt booleanDt = (BooleanDt) o;
return booleanDt.getValue();
}
if (o instanceof IBaseBooleanDatatype) {
IBaseBooleanDatatype booleanValue = (IBaseBooleanDatatype) o;
return booleanValue.getValue();
}
if (o instanceof Boolean) {
return (Boolean) o;
}
throw new RuntimeException(Msg.code(2342) + "unsupported type :" + theValue.getClass().getName());
} catch (IllegalAccessException theException) {
// should never happen - all Coding models have this field.
throw new RuntimeException(Msg.code(2343) + "illegal access during reflection", theException);
}
}
private Field getSpy(IBaseCoding theValue) {
return mySpies.computeIfAbsent(theValue.getClass(), k -> getFieldHandle(k));
}
private static Field getFieldHandle(Class k) {
Field result = FieldUtils.getField(k, "userSelected", true);
if (result == null) {
result = FieldUtils.getField(k, "myUserSelected", true);
}
return result;
}
}

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
import ca.uhn.fhir.model.api.IResource;
@ -72,7 +73,7 @@ import java.util.Date;
import java.util.List;
import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.decodeResource;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class JpaStorageResourceParser implements IJpaStorageResourceParser {
@ -350,19 +351,26 @@ public class JpaStorageResourceParser implements IJpaStorageResourceParser {
List<IBaseCoding> securityLabels = new ArrayList<>();
List<IdDt> profiles = new ArrayList<>();
for (BaseTag next : theTagList) {
switch (next.getTag().getTagType()) {
TagDefinition nextTag = next.getTag();
switch (nextTag.getTagType()) {
case PROFILE:
profiles.add(new IdDt(next.getTag().getCode()));
profiles.add(new IdDt(nextTag.getCode()));
break;
case SECURITY_LABEL:
IBaseCoding secLabel = (IBaseCoding) myFhirContext.getVersion().newCodingDt();
secLabel.setSystem(next.getTag().getSystem());
secLabel.setCode(next.getTag().getCode());
secLabel.setDisplay(next.getTag().getDisplay());
secLabel.setSystem(nextTag.getSystem());
secLabel.setCode(nextTag.getCode());
secLabel.setDisplay(nextTag.getDisplay());
// wipmb these technically support userSelected and version
securityLabels.add(secLabel);
break;
case TAG:
tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay()));
// wipmb check xml, etc.
Tag e = new Tag(nextTag.getSystem(), nextTag.getCode(), nextTag.getDisplay());
e.setVersion(nextTag.getVersion());
// careful! These are Boolean, not boolean.
e.setUserSelectedBoolean(nextTag.getUserSelected());
tagList.add(e);
break;
}
}
@ -434,7 +442,12 @@ public class JpaStorageResourceParser implements IJpaStorageResourceParser {
tag.setCode(next.getTag().getCode());
tag.setDisplay(next.getTag().getDisplay());
tag.setVersion(next.getTag().getVersion());
tag.setUserSelected(next.getTag().getUserSelected());
Boolean userSelected = next.getTag().getUserSelected();
// the tag is created with a null userSelected, but the api is primitive boolean.
// Only update if we are non-null.
if (nonNull(userSelected)) {
tag.setUserSelected(userSelected);
}
break;
}
}

View File

@ -101,13 +101,26 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
protected void init680() {
Builder version = forVersion(VersionEnum.V6_8_0);
// HAPI-FHIR #4801 - Add New Index On HFJ_RESOURCE
Builder.BuilderWithTableName resourceTable = version.onTable("HFJ_RESOURCE");
resourceTable
.addIndex("20230502.1", "IDX_RES_RESID_UPDATED")
// HAPI-FHIR #4801 - Add New Index On HFJ_RESOURCE
Builder.BuilderWithTableName resourceTable = version.onTable("HFJ_RESOURCE");
resourceTable
.addIndex("20230502.1", "IDX_RES_RESID_UPDATED")
.unique(false)
.online(true)
.withColumns("RES_ID", "RES_UPDATED", "PARTITION_ID");
Builder.BuilderWithTableName tagDefTable = version.onTable("HFJ_TAG_DEF");
tagDefTable.dropIndex("20230505.1", "IDX_TAGDEF_TYPESYSCODEVERUS");
tagDefTable.dropIndex("20230505.2", "IDX_TAG_DEF_TP_CD_SYS");
tagDefTable
.addIndex("20230505.3", "IDX_TAG_DEF_TP_CD_SYS")
.unique(false)
.online(true)
.withColumns("RES_ID", "RES_UPDATED", "PARTITION_ID");
.online(false)
.withColumns("TAG_TYPE", "TAG_CODE", "TAG_SYSTEM", "TAG_ID", "TAG_VERSION", "TAG_USER_SELECTED");
}
protected void init660() {
@ -362,8 +375,14 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.online(true)
.withColumns("SEARCH_PID")
.onlyAppliesToPlatforms(NON_AUTOMATIC_FK_INDEX_PLATFORMS);
{ //We added this constraint when userSelected and Version were added. It is no longer necessary.
Builder.BuilderWithTableName tagDefTable = version.onTable("HFJ_TAG_DEF");
tagDefTable.dropIndex("20230503.1", "IDX_TAGDEF_TYPESYSCODEVERUS");
}
}
private void init620() {
Builder version = forVersion(VersionEnum.V6_2_0);

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.jpa.dao;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Coding;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import javax.annotation.Nonnull;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class CodingSpyTest {
/**
* Ensure we can read the default null value of userSelected on Coding
*/
@ParameterizedTest
@MethodSource("getCases")
void canReadValueUserSelected(IBaseCoding theObject, Boolean theValue) {
IBaseCoding value = theObject.setSystem("http://example.com").setCode("value");
if (theValue != null) {
theObject.setUserSelected(theValue);
}
Boolean result = new CodingSpy().getBooleanObject(theObject);
assertEquals(theValue, result);
}
@Test
void canReadNullUserSelected() {
Coding value = new Coding().setSystem("http://example.com").setCode("value");
Boolean result = new CodingSpy().getBooleanObject(value);
assertNull(result);
}
static List<Arguments> getCases() {
var classes = List.of(
org.hl7.fhir.r4.model.Coding.class,
org.hl7.fhir.r5.model.Coding.class,
ca.uhn.fhir.model.api.Tag.class
);
var values = Lists.newArrayList(true, false, null);
return classes.stream()
.flatMap(k-> values.stream()
.map(v-> Arguments.of(getNewInstance(k), v)))
.toList();
}
@Nonnull
private static Object getNewInstance(Class<? extends IBaseCoding> k) {
try {
return k.getDeclaredConstructor().newInstance();
} catch (Exception theE) {
throw new RuntimeException(theE);
}
}
@Test
void booleanNulls() {
// given
BooleanType b = new BooleanType();
// when
var s = b.asStringValue();
assertThat(s, blankOrNullString());
}
}

View File

@ -46,11 +46,7 @@ import java.util.Collection;
@Table(
name = "HFJ_TAG_DEF",
indexes = {
@Index(name = "IDX_TAG_DEF_TP_CD_SYS", columnList = "TAG_TYPE, TAG_CODE, TAG_SYSTEM, TAG_ID"),
},
uniqueConstraints = {
@UniqueConstraint(name = "IDX_TAGDEF_TYPESYSCODEVERUS",
columnNames = {"TAG_TYPE", "TAG_SYSTEM", "TAG_CODE", "TAG_VERSION", "TAG_USER_SELECTED"})
@Index(name = "IDX_TAG_DEF_TP_CD_SYS", columnList = "TAG_TYPE, TAG_CODE, TAG_SYSTEM, TAG_ID, TAG_VERSION, TAG_USER_SELECTED"),
}
)
public class TagDefinition implements Serializable {
@ -161,14 +157,16 @@ public class TagDefinition implements Serializable {
}
}
/**
* Warning - this is nullable, while IBaseCoding getUserSelected isn't.
* wipmb maybe rename?
*/
public Boolean getUserSelected() {
// TODO: LD: this is not ideal as we are implicitly assuming null is false.
// Ideally we should fix IBaseCoding to return wrapper Boolean but that will involve another core/hapi release
return myUserSelected != null ? myUserSelected : false;
return myUserSelected;
}
public void setUserSelected(Boolean theUserSelected) {
myUserSelected = theUserSelected != null && theUserSelected;
myUserSelected = theUserSelected;
}

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.system.HapiTestSystemProperties;
import ch.qos.logback.classic.Level;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Coding;
@ -41,6 +42,8 @@ import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ -153,6 +156,30 @@ public class FhirResourceDaoR4MetaTest extends BaseJpaR4Test {
assertEquals("bar", patient2.getMeta().getSecurityFirstRep().getCode());
}
/**
* Make sure round-trips with tags don't add a userSelected property.
* wipmb tag userSelected test
* Verify https://github.com/hapifhir/hapi-fhir/issues/4819
*/
@Test
void testAddTag_userSelectedStaysNull() {
// given
Patient patient1 = new Patient();
patient1.getMeta().addTag().setSystem("http://foo").setCode("bar");
patient1.setActive(true);
IIdType pid1 = myPatientDao.create(patient1).getId();
// when
var patientReadback = myPatientDao.read(pid1);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patientReadback));
// then
var tag = patientReadback.getMeta().getTag().get(0);
assertNotNull(tag);
assertNull(tag.getUserSelectedElement().asStringValue());
}
@Nested
public class TestTagWithVersionAndUserSelected {
@ -204,6 +231,7 @@ public class FhirResourceDaoR4MetaTest extends BaseJpaR4Test {
.setSystem(expectedSystem1)
.setCode(expectedCode1)
.setDisplay(expectedDisplay1);
assertNull(newTag.getUserSelectedElement().asStringValue());
assertFalse(newTag.getUserSelected());
newTag.setVersion(expectedVersion1)
.setUserSelected(expectedUserSelected1);

View File

@ -5,6 +5,8 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
@ -96,6 +98,33 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test {
}
@Test
public void testTagCollision() {
TagDefinition def = new TagDefinition();
def.setTagType(TagTypeEnum.TAG);
def.setSystem("system");
def.setCode("coding");
def.setDisplay("display");
def.setUserSelected(null);
Patient p = new Patient();
p.getMeta().addTag("system", "coding", "display");
myMemoryCacheService.invalidateAllCaches();
myPatientDao.create(p, new SystemRequestDetails());
//inject conflicting.
myTagDefinitionDao.saveAndFlush(def);
myMemoryCacheService.invalidateAllCaches();
myPatientDao.create(p, new SystemRequestDetails());
myMemoryCacheService.invalidateAllCaches();
myPatientDao.create(p, new SystemRequestDetails());
}
@Test
public void testCreateAndUpdateWithoutRequest() {
String methodName = "testUpdateByUrl";