001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 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;
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 myPayloadDecoded;
060
061        @JsonIgnore
062        protected transient String myPayloadType;
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        public void setPayloadId(IIdType thePayloadId) {
121                myPayloadId = null;
122                if (thePayloadId != null) {
123                        myPayloadId = thePayloadId.toUnqualifiedVersionless().getValue();
124                        myPayloadVersion = thePayloadId.getVersionIdPart();
125                }
126        }
127
128        /**
129         * @deprecated Use {@link #getPayloadId()} instead. Deprecated in 5.6.0 / 2021-10-27
130         */
131        public String getId() {
132                return myPayloadId;
133        }
134
135        /**
136         * @deprecated Use {@link #setPayloadId(IIdType)}. Deprecated in 5.6.0 / 2021-10-27
137         */
138        @Deprecated
139        public void setId(IIdType theId) {
140                setPayloadId(theId);
141        }
142
143        /**
144         * @deprecated Use {@link #getPayloadId(FhirContext)}. Deprecated in 5.6.0 / 2021-10-27
145         */
146        public IIdType getId(FhirContext theCtx) {
147                return getPayloadId(theCtx);
148        }
149
150        /**
151         * @since 5.6.0
152         */
153        public IIdType getPayloadId(FhirContext theCtx) {
154                IIdType retVal = null;
155
156                if (myPayloadId != null) {
157                        retVal = theCtx.getVersion().newIdType().setValue(myPayloadId).withVersion(myPayloadVersion);
158                }
159
160                return retVal;
161        }
162
163        @Nullable
164        public IBaseResource getNewPayload(FhirContext theCtx) {
165                if (myPayloadDecoded == null && isNotBlank(myPayload)) {
166                        myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload);
167                }
168                return myPayloadDecoded;
169        }
170
171        @Nullable
172        public IBaseResource getPayload(FhirContext theCtx) {
173                IBaseResource retVal = myPayloadDecoded;
174                if (retVal == null && isNotBlank(myPayload)) {
175                        IParser parser = EncodingEnum.detectEncoding(myPayload).newParser(theCtx);
176                        retVal = parser.parseResource(myPayload);
177                        myPayloadDecoded = retVal;
178                }
179                return retVal;
180        }
181
182        @Nonnull
183        public String getPayloadString() {
184                if (this.myPayload != null) {
185                        return this.myPayload;
186                }
187
188                return "";
189        }
190
191        public void setNewPayload(FhirContext theCtx, IBaseResource thePayload) {
192                /*
193                 * References with placeholders would be invalid by the time we get here, and
194                 * would be caught before we even get here. This check is basically a last-ditch
195                 * effort to make sure nothing has broken in the various safeguards that
196                 * should prevent this from happening (hence it only being an assert as
197                 * opposed to something executed all the time).
198                 */
199                assert payloadContainsNoPlaceholderReferences(theCtx, thePayload);
200
201                /*
202                 * Note: Don't set myPayloadDecoded in here- This is a false optimization since
203                 * it doesn't actually get used if anyone is doing subscriptions at any
204                 * scale using a queue engine, and not going through the serialize/deserialize
205                 * as we would in a queue engine can mask bugs.
206                 * -JA
207                 */
208                myPayload = theCtx.newJsonParser().encodeResourceToString(thePayload);
209
210                setPayloadIdFromPayload(theCtx, thePayload);
211        }
212
213        private void setPayloadIdFromPayload(FhirContext theCtx, IBaseResource thePayload) {
214                IIdType payloadIdType = thePayload.getIdElement().toUnqualified();
215                if (!payloadIdType.hasResourceType()) {
216                        String resourceType = theCtx.getResourceType(thePayload);
217                        payloadIdType = payloadIdType.withResourceType(resourceType);
218                }
219
220                setPayloadId(payloadIdType);
221        }
222
223        public RequestPartitionId getPartitionId() {
224                return myPartitionId;
225        }
226
227        public void setPartitionId(RequestPartitionId thePartitionId) {
228                myPartitionId = thePartitionId;
229        }
230
231        @Override
232        public String toString() {
233                return new ToStringBuilder(this)
234                                .append("operationType", myOperationType)
235                                .append("partitionId", myPartitionId)
236                                .append("payloadId", myPayloadId)
237                                .toString();
238        }
239
240        protected static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) {
241                List<ResourceReferenceInfo> refs = theCtx.newTerser().getAllResourceReferences(theNewPayload);
242                for (ResourceReferenceInfo next : refs) {
243                        String ref = next.getResourceReference().getReferenceElement().getValue();
244                        if (isBlank(ref)) {
245                                IBaseResource resource = next.getResourceReference().getResource();
246                                if (resource != null) {
247                                        ref = resource.getIdElement().getValue();
248                                }
249                        }
250                        if (isNotBlank(ref)) {
251                                if (ref.startsWith("#")) {
252                                        continue;
253                                }
254                                if (ref.startsWith("urn:uuid:")) {
255                                        throw new AssertionError(Msg.code(320) + "Reference at " + next.getName() + " is invalid: " + ref);
256                                }
257                        }
258                }
259                return true;
260        }
261
262        @Nullable
263        @Override
264        public String getMessageKeyOrDefault() {
265                return StringUtils.defaultString(super.getMessageKeyOrNull(), myPayloadId);
266        }
267
268        public boolean hasPayloadType(FhirContext theFhirContext, @Nonnull String theResourceName) {
269                if (myPayloadType == null) {
270                        myPayloadType = getPayloadType(theFhirContext);
271                }
272                return theResourceName.equals(myPayloadType);
273        }
274
275        @Nullable
276        public String getPayloadType(FhirContext theFhirContext) {
277                String retval = null;
278                IIdType payloadId = getPayloadId(theFhirContext);
279                if (payloadId != null) {
280                        retval = payloadId.getResourceType();
281                }
282                if (isBlank(retval)) {
283                        IBaseResource payload = getNewPayload(theFhirContext);
284                        if (payload != null) {
285                                retval = theFhirContext.getResourceType(payload);
286                        }
287                }
288                return retval;
289        }
290}