Skip to content
48 changes: 20 additions & 28 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Tag.java
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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() {
}
Expand Down Expand Up @@ -114,37 +115,22 @@ public boolean equals(Object obj) {
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;
}

Expand Down Expand Up @@ -234,12 +220,18 @@ public IBaseCoding setVersion(String theVersion) {
}

@Override
public boolean getUserSelected() { return myUserSelected; }
public boolean getUserSelected() { return myUserSelected != null && myUserSelected; }

public Boolean getUserSelectedBoolean() { return myUserSelected; }

@Override
public IBaseCoding setUserSelected(boolean theUserSelected) {
myUserSelected = theUserSelected;
return this;
}

public void setUserSelectedBoolean(Boolean theUserSelected) {
myUserSelected = theUserSelected;
}

}
123 changes: 64 additions & 59 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -724,78 +724,83 @@ private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDe
}

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);
parseMetaForDSTU2(theResDef, theResource, theEventWriter, theContainedResource, theEncodeContext, resDef);
}

if (super.shouldEncodeResourceMeta(resource) && (ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) || !extensionMetadataKeys.isEmpty()) {
beginObject(theEventWriter, "meta");
encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef, theEncodeContext), theEncodeContext);

if (shouldEncodePath(resource, "meta.versionId")) {
writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart);
}
if (shouldEncodePath(resource, "meta.lastUpdated")) {
writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated);
}
theEventWriter.endObject();
}

if (profiles != null && profiles.isEmpty() == false) {
beginArray(theEventWriter, "profile");
for (IIdType profile : profiles) {
if (profile != null && isNotBlank(profile.getValue())) {
theEventWriter.write(profile.getValue());
}
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();
}
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 (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();
if (tags != null && tags.isEmpty() == false) {
beginArray(theEventWriter, "tag");
for (Tag tag : tags) {
if (tag.isEmpty()) {
continue;
}
theEventWriter.endArray();
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();
}

addExtensionMetadata(theResDef, theResource, theContainedResource, extensionMetadataKeys, resDef, theEventWriter, theEncodeContext);

theEventWriter.endObject(); // end meta
theEventWriter.endArray();
}
}

encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef, theEncodeContext), theEncodeContext);
addExtensionMetadata(theResDef, theResource, theContainedResource, extensionMetadataKeys, resDef, theEventWriter, theEncodeContext);

theEventWriter.endObject();
theEventWriter.endObject(); // end meta
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void testEquals() {
@Test
public void testHashCode() {
Tag tag1 = new Tag().setScheme("scheme").setTerm("term").setLabel("label");
assertEquals(-1029266947, tag1.hashCode());
assertEquals(-1029268184, tag1.hashCode());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ public Coding codingToCanonical(IBaseCoding theCoding) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: fix
issue: 4819
title: "Tags no longer provide a default `false` value for `userSelected`."
Original file line number Diff line number Diff line change
@@ -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."
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -283,7 +285,7 @@ private void extractTagsHapi(TransactionDetails theTransactionDetails, IResource
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);
Expand Down Expand Up @@ -323,7 +325,7 @@ private void extractTagsRi(TransactionDetails theTransactionDetails, IAnyResourc
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);
Expand All @@ -335,7 +337,7 @@ private void extractTagsRi(TransactionDetails theTransactionDetails, IAnyResourc
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);
Expand Down Expand Up @@ -394,7 +396,6 @@ protected TagDefinition getTagOrNull(TransactionDetails theTransactionDetails, T
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);
Expand Down Expand Up @@ -425,6 +426,7 @@ private TagDefinition getOrCreateTag(TagTypeEnum theTagType, String theScheme, S
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);
Expand Down Expand Up @@ -525,9 +527,9 @@ private TypedQuery<TagDefinition> buildTagQuery(TagTypeEnum theTagType, String t
? 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);
Expand Down Expand Up @@ -817,12 +819,14 @@ private boolean updateTags(TransactionDetails theTransactionDetails, RequestDeta

// 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource
}
}


if (!hasTag) {
theEntity.setHasTags(true);

Expand Down
Loading