
001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.messaging; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.model.api.IModelJson; 026import ca.uhn.fhir.parser.IParser; 027import ca.uhn.fhir.rest.api.EncodingEnum; 028import ca.uhn.fhir.rest.api.server.RequestDetails; 029import ca.uhn.fhir.util.ResourceReferenceInfo; 030import com.fasterxml.jackson.annotation.JsonIgnore; 031import com.fasterxml.jackson.annotation.JsonProperty; 032import jakarta.annotation.Nonnull; 033import jakarta.annotation.Nullable; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.lang3.builder.ToStringBuilder; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037import org.hl7.fhir.instance.model.api.IIdType; 038 039import java.util.List; 040import java.util.Objects; 041 042import static org.apache.commons.lang3.StringUtils.isBlank; 043import static org.apache.commons.lang3.StringUtils.isNotBlank; 044 045public abstract class BaseResourceModifiedMessage extends BaseResourceMessage implements IResourceMessage, IModelJson { 046 047 @JsonProperty("payload") 048 protected String myPayload; 049 050 @JsonProperty("payloadId") 051 protected String myPayloadId; 052 053 @JsonProperty(value = "partitionId") 054 protected RequestPartitionId myPartitionId; 055 056 @JsonProperty(value = "payloadVersion") 057 protected String myPayloadVersion; 058 059 @JsonIgnore 060 protected transient IBaseResource myPayloadDecoded; 061 062 @JsonIgnore 063 protected transient String myPayloadType; 064 065 /** 066 * Constructor 067 */ 068 public BaseResourceModifiedMessage() { 069 super(); 070 } 071 072 public BaseResourceModifiedMessage(IIdType theIdType, OperationTypeEnum theOperationType) { 073 this(); 074 setOperationType(theOperationType); 075 setPayloadId(theIdType); 076 } 077 078 public BaseResourceModifiedMessage( 079 FhirContext theFhirContext, IBaseResource theResource, OperationTypeEnum theOperationType) { 080 this(); 081 setOperationType(theOperationType); 082 setNewPayload(theFhirContext, theResource); 083 } 084 085 public BaseResourceModifiedMessage( 086 FhirContext theFhirContext, 087 IBaseResource theNewResource, 088 OperationTypeEnum theOperationType, 089 RequestDetails theRequest) { 090 this(theFhirContext, theNewResource, theOperationType); 091 if (theRequest != null) { 092 setTransactionId(theRequest.getTransactionGuid()); 093 } 094 } 095 096 public BaseResourceModifiedMessage( 097 FhirContext theFhirContext, 098 IBaseResource theNewResource, 099 OperationTypeEnum theOperationType, 100 RequestDetails theRequest, 101 RequestPartitionId theRequestPartitionId) { 102 this(theFhirContext, theNewResource, theOperationType); 103 if (theRequest != null) { 104 setTransactionId(theRequest.getTransactionGuid()); 105 } 106 myPartitionId = theRequestPartitionId; 107 } 108 109 @Override 110 public String getPayloadId() { 111 return myPayloadId; 112 } 113 114 public String getPayloadVersion() { 115 return myPayloadVersion; 116 } 117 118 /** 119 * @since 5.6.0 120 */ 121 public void setPayloadId(IIdType thePayloadId) { 122 myPayloadId = null; 123 if (thePayloadId != null) { 124 myPayloadId = thePayloadId.toUnqualifiedVersionless().getValue(); 125 myPayloadVersion = thePayloadId.getVersionIdPart(); 126 } 127 } 128 129 /** 130 * @deprecated Use {@link #getPayloadId()} instead. Deprecated in 5.6.0 / 2021-10-27 131 */ 132 public String getId() { 133 return myPayloadId; 134 } 135 136 /** 137 * @deprecated Use {@link #setPayloadId(IIdType)}. Deprecated in 5.6.0 / 2021-10-27 138 */ 139 @Deprecated 140 public void setId(IIdType theId) { 141 setPayloadId(theId); 142 } 143 144 /** 145 * @deprecated Use {@link #getPayloadId(FhirContext)}. Deprecated in 5.6.0 / 2021-10-27 146 */ 147 public IIdType getId(FhirContext theCtx) { 148 return getPayloadId(theCtx); 149 } 150 151 /** 152 * @since 5.6.0 153 */ 154 public IIdType getPayloadId(FhirContext theCtx) { 155 IIdType retVal = null; 156 157 if (myPayloadId != null) { 158 retVal = theCtx.getVersion().newIdType().setValue(myPayloadId).withVersion(myPayloadVersion); 159 } 160 161 return retVal; 162 } 163 164 @Nullable 165 public IBaseResource getNewPayload(FhirContext theCtx) { 166 if (myPayloadDecoded == null && isNotBlank(myPayload)) { 167 myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload); 168 } 169 return myPayloadDecoded; 170 } 171 172 @Nullable 173 public IBaseResource getPayload(FhirContext theCtx) { 174 IBaseResource retVal = myPayloadDecoded; 175 if (retVal == null && isNotBlank(myPayload)) { 176 IParser parser = EncodingEnum.detectEncoding(myPayload).newParser(theCtx); 177 retVal = parser.parseResource(myPayload); 178 myPayloadDecoded = retVal; 179 } 180 return retVal; 181 } 182 183 @Nonnull 184 public String getPayloadString() { 185 if (this.myPayload != null) { 186 return this.myPayload; 187 } 188 189 return ""; 190 } 191 192 public void setNewPayload(FhirContext theCtx, IBaseResource thePayload) { 193 /* 194 * References with placeholders would be invalid by the time we get here, and 195 * would be caught before we even get here. This check is basically a last-ditch 196 * effort to make sure nothing has broken in the various safeguards that 197 * should prevent this from happening (hence it only being an assert as 198 * opposed to something executed all the time). 199 */ 200 assert payloadContainsNoPlaceholderReferences(theCtx, thePayload); 201 202 /* 203 * Note: Don't set myPayloadDecoded in here- This is a false optimization since 204 * it doesn't actually get used if anyone is doing subscriptions at any 205 * scale using a queue engine, and not going through the serialize/deserialize 206 * as we would in a queue engine can mask bugs. 207 * -JA 208 */ 209 myPayload = theCtx.newJsonParser().encodeResourceToString(thePayload); 210 211 setPayloadIdFromPayload(theCtx, thePayload); 212 } 213 214 private void setPayloadIdFromPayload(FhirContext theCtx, IBaseResource thePayload) { 215 IIdType payloadIdType = thePayload.getIdElement().toUnqualified(); 216 if (!payloadIdType.hasResourceType()) { 217 String resourceType = theCtx.getResourceType(thePayload); 218 payloadIdType = payloadIdType.withResourceType(resourceType); 219 } 220 221 setPayloadId(payloadIdType); 222 } 223 224 public RequestPartitionId getPartitionId() { 225 return myPartitionId; 226 } 227 228 public void setPartitionId(RequestPartitionId thePartitionId) { 229 myPartitionId = thePartitionId; 230 } 231 232 @Override 233 public String toString() { 234 return new ToStringBuilder(this) 235 .append("operationType", myOperationType) 236 .append("partitionId", myPartitionId) 237 .append("payloadId", myPayloadId) 238 .toString(); 239 } 240 241 protected static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) { 242 List<ResourceReferenceInfo> refs = theCtx.newTerser().getAllResourceReferences(theNewPayload); 243 for (ResourceReferenceInfo next : refs) { 244 String ref = next.getResourceReference().getReferenceElement().getValue(); 245 if (isBlank(ref)) { 246 IBaseResource resource = next.getResourceReference().getResource(); 247 if (resource != null) { 248 ref = resource.getIdElement().getValue(); 249 } 250 } 251 if (isNotBlank(ref)) { 252 if (ref.startsWith("#")) { 253 continue; 254 } 255 if (ref.startsWith("urn:uuid:")) { 256 throw new AssertionError(Msg.code(320) + "Reference at " + next.getName() + " is invalid: " + ref); 257 } 258 } 259 } 260 return true; 261 } 262 263 @Nullable 264 @Override 265 public String getMessageKeyOrDefault() { 266 return StringUtils.defaultString(super.getMessageKeyOrNull(), myPayloadId); 267 } 268 269 public boolean hasPayloadType(FhirContext theFhirContext, @Nonnull String theResourceName) { 270 if (myPayloadType == null) { 271 myPayloadType = getPayloadType(theFhirContext); 272 } 273 return theResourceName.equals(myPayloadType); 274 } 275 276 @Nullable 277 public String getPayloadType(FhirContext theFhirContext) { 278 String retval = null; 279 IIdType payloadId = getPayloadId(theFhirContext); 280 if (payloadId != null) { 281 retval = payloadId.getResourceType(); 282 } 283 if (isBlank(retval)) { 284 IBaseResource payload = getNewPayload(theFhirContext); 285 if (payload != null) { 286 retval = theFhirContext.getResourceType(payload); 287 } 288 } 289 return retval; 290 } 291 292 @Override 293 public boolean equals(Object theO) { 294 if (this == theO) return true; 295 if (theO == null || getClass() != theO.getClass()) return false; 296 if (!super.equals(theO)) return false; 297 BaseResourceModifiedMessage that = (BaseResourceModifiedMessage) theO; 298 return Objects.equals(myPayload, that.myPayload) && Objects.equals(getPayloadId(), that.getPayloadId()); 299 } 300 301 @Override 302 public int hashCode() { 303 return Objects.hash(super.hashCode(), myPayload, getPayloadId()); 304 } 305}