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.builder.ToStringBuilder;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.hl7.fhir.instance.model.api.IIdType;
037
038import java.util.List;
039import java.util.Objects;
040
041import static org.apache.commons.lang3.StringUtils.isBlank;
042import static org.apache.commons.lang3.StringUtils.isNotBlank;
043
044public abstract class BaseResourceModifiedMessage extends BaseResourceMessage implements IResourceMessage, IModelJson {
045
046        @JsonProperty("payload")
047        protected String myPayload;
048
049        @JsonProperty("payloadId")
050        protected String myPayloadId;
051
052        @JsonProperty(value = "partitionId")
053        protected RequestPartitionId myPartitionId;
054
055        @JsonProperty(value = "payloadVersion")
056        protected String myPayloadVersion;
057
058        @JsonIgnore
059        protected transient IBaseResource myResourceDecoded;
060
061        @JsonIgnore
062        protected transient String myResourceType;
063
064        /**
065         * Constructor
066         */
067        public BaseResourceModifiedMessage() {
068                super();
069        }
070
071        public BaseResourceModifiedMessage(IIdType theIdType, OperationTypeEnum theOperationType) {
072                this();
073                setOperationType(theOperationType);
074                setPayloadId(theIdType);
075        }
076
077        public BaseResourceModifiedMessage(
078                        FhirContext theFhirContext, IBaseResource theResource, OperationTypeEnum theOperationType) {
079                this();
080                setOperationType(theOperationType);
081                setNewPayload(theFhirContext, theResource);
082        }
083
084        public BaseResourceModifiedMessage(
085                        FhirContext theFhirContext,
086                        IBaseResource theNewResource,
087                        OperationTypeEnum theOperationType,
088                        RequestDetails theRequest) {
089                this(theFhirContext, theNewResource, theOperationType);
090                if (theRequest != null) {
091                        setTransactionId(theRequest.getTransactionGuid());
092                }
093        }
094
095        public BaseResourceModifiedMessage(
096                        FhirContext theFhirContext,
097                        IBaseResource theNewResource,
098                        OperationTypeEnum theOperationType,
099                        RequestDetails theRequest,
100                        RequestPartitionId theRequestPartitionId) {
101                this(theFhirContext, theNewResource, theOperationType);
102                if (theRequest != null) {
103                        setTransactionId(theRequest.getTransactionGuid());
104                }
105                myPartitionId = theRequestPartitionId;
106        }
107
108        @Override
109        public String getPayloadId() {
110                return myPayloadId;
111        }
112
113        public String getPayloadVersion() {
114                return myPayloadVersion;
115        }
116
117        /**
118         * @since 5.6.0
119         */
120        @Override
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 getNewResource(FhirContext theCtx) {
166                if (myResourceDecoded == null && isNotBlank(myPayload)) {
167                        myResourceDecoded = theCtx.newJsonParser().parseResource(myPayload);
168                }
169                return myResourceDecoded;
170        }
171
172        @Nullable
173        public IBaseResource getResource(FhirContext theCtx) {
174                IBaseResource retVal = myResourceDecoded;
175                if (retVal == null && isNotBlank(myPayload)) {
176                        IParser parser = EncodingEnum.detectEncoding(myPayload).newParser(theCtx);
177                        retVal = parser.parseResource(myPayload);
178                        myResourceDecoded = 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 getPayloadMessageKey() {
266                return Objects.toString(super.getPayloadMessageKey(), myPayloadId);
267        }
268
269        public boolean hasResourceType(FhirContext theFhirContext, @Nonnull String theResourceName) {
270                if (myResourceType == null) {
271                        myResourceType = getResourceType(theFhirContext);
272                }
273                return theResourceName.equals(myResourceType);
274        }
275
276        @Nullable
277        public String getResourceType(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 = getNewResource(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}