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}