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.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.model.api.IModelJson;
025import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
026import com.fasterxml.jackson.annotation.JsonProperty;
027import com.google.common.annotations.VisibleForTesting;
028import jakarta.annotation.Nullable;
029import org.apache.commons.lang3.ObjectUtils;
030import org.apache.commons.lang3.Validate;
031
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Optional;
037
038@SuppressWarnings("WeakerAccess")
039public abstract class BaseResourceMessage implements IResourceMessage, IModelJson {
040
041        @JsonProperty("operationType")
042        protected BaseResourceModifiedMessage.OperationTypeEnum myOperationType;
043
044        @JsonProperty("attributes")
045        private Map<String, String> myAttributes;
046
047        @JsonProperty("transactionId")
048        private String myTransactionId;
049
050        @JsonProperty("mediaType")
051        private String myMediaType;
052
053        @JsonProperty("messageKey")
054        private String myPayloadMessageKey;
055
056        public abstract RequestPartitionId getPartitionId();
057
058        public abstract void setPartitionId(RequestPartitionId thePartitionId);
059
060        /**
061         * Returns an attribute stored in this message.
062         * <p>
063         * Attributes are just a spot for user data of any kind to be
064         * added to the message for pasing along the subscription processing
065         * pipeline (typically by interceptors). Values will be carried from the beginning to the end.
066         * </p>
067         * <p>
068         * Note that messages are designed to be passed into queueing systems
069         * and serialized as JSON. As a result, only strings are currently allowed
070         * as values.
071         * </p>
072         */
073        public Optional<String> getAttribute(String theKey) {
074                Validate.notBlank(theKey);
075                if (myAttributes == null) {
076                        return Optional.empty();
077                }
078                return Optional.ofNullable(myAttributes.get(theKey));
079        }
080
081        /**
082         * Sets an attribute stored in this message.
083         * <p>
084         * Attributes are just a spot for user data of any kind to be
085         * added to the message for passing along the subscription processing
086         * pipeline (typically by interceptors). Values will be carried from the beginning to the end.
087         * </p>
088         * <p>
089         * Note that messages are designed to be passed into queueing systems
090         * and serialized as JSON. As a result, only strings are currently allowed
091         * as values.
092         * </p>
093         *
094         * @param theKey   The key (must not be null or blank)
095         * @param theValue The value (must not be null)
096         */
097        public void setAttribute(String theKey, String theValue) {
098                Validate.notBlank(theKey);
099                Objects.requireNonNull(theValue);
100                if (myAttributes == null) {
101                        myAttributes = new HashMap<>();
102                }
103                myAttributes.put(theKey, theValue);
104        }
105
106        /**
107         * Copies any attributes from the given message into this messsage.
108         *
109         * @see #setAttribute(String, String)
110         * @see #getAttribute(String)
111         */
112        public void copyAdditionalPropertiesFrom(BaseResourceMessage theMsg) {
113                if (theMsg.myAttributes != null) {
114                        if (myAttributes == null) {
115                                myAttributes = new HashMap<>();
116                        }
117                        myAttributes.putAll(theMsg.myAttributes);
118                }
119        }
120
121        /**
122         * Returns the {@link OperationTypeEnum} that is occurring to the Resource of the message
123         *
124         * @return the operation type.
125         */
126        public BaseResourceModifiedMessage.OperationTypeEnum getOperationType() {
127                return myOperationType;
128        }
129
130        /**
131         * Sets the {@link OperationTypeEnum} occuring to the resource of the message.
132         *
133         * @param theOperationType The operation type to set.
134         */
135        public void setOperationType(BaseResourceModifiedMessage.OperationTypeEnum theOperationType) {
136                myOperationType = theOperationType;
137        }
138
139        /**
140         * Retrieve the transaction ID related to this message.
141         *
142         * @return the transaction ID, or null.
143         */
144        @Nullable
145        public String getTransactionId() {
146                return myTransactionId;
147        }
148
149        /**
150         * Adds a transaction ID to this message. This ID can be used for many purposes. For example, performing tracing
151         * across asynchronous hooks, tying data together, or downstream logging purposes.
152         * <p>
153         * One current internal implementation uses this field to tie back MDM processing results (which are asynchronous)
154         * to the original transaction log that caused the MDM processing to occur.
155         *
156         * @param theTransactionId An ID representing a transaction of relevance to this message.
157         */
158        public void setTransactionId(String theTransactionId) {
159                myTransactionId = theTransactionId;
160        }
161
162        public String getMediaType() {
163                return myMediaType;
164        }
165
166        public void setMediaType(String theMediaType) {
167                myMediaType = theMediaType;
168        }
169
170        @Override
171        @Nullable
172        public String getPayloadMessageKey() {
173                return myPayloadMessageKey;
174        }
175
176        @Override
177        public void setPayloadMessageKey(String thePayloadMessageKey) {
178                myPayloadMessageKey = thePayloadMessageKey;
179        }
180
181        public enum OperationTypeEnum {
182                CREATE(RestOperationTypeEnum.CREATE),
183                UPDATE(RestOperationTypeEnum.UPDATE),
184                DELETE(RestOperationTypeEnum.DELETE),
185                MANUALLY_TRIGGERED(RestOperationTypeEnum.UPDATE),
186                TRANSACTION(RestOperationTypeEnum.UPDATE);
187
188                private final RestOperationTypeEnum myRestOperationTypeEnum;
189
190                OperationTypeEnum(RestOperationTypeEnum theRestOperationTypeEnum) {
191                        myRestOperationTypeEnum = theRestOperationTypeEnum;
192                }
193
194                public static OperationTypeEnum from(RestOperationTypeEnum theRestOperationType) {
195                        switch (theRestOperationType) {
196                                case CREATE:
197                                        return CREATE;
198                                case UPDATE:
199                                        return UPDATE;
200                                case DELETE:
201                                        return DELETE;
202                                default:
203                                        throw new IllegalArgumentException(
204                                                        Msg.code(2348) + "Unsupported operation type: " + theRestOperationType);
205                        }
206                }
207
208                public RestOperationTypeEnum asRestOperationType() {
209                        return myRestOperationTypeEnum;
210                }
211        }
212
213        @VisibleForTesting
214        public Map<String, String> getAttributes() {
215                return ObjectUtils.defaultIfNull(myAttributes, Collections.emptyMap());
216        }
217
218        @Override
219        public boolean equals(Object theO) {
220                if (this == theO) return true;
221                if (theO == null || getClass() != theO.getClass()) return false;
222                BaseResourceMessage that = (BaseResourceMessage) theO;
223                return getOperationType() == that.getOperationType()
224                                && Objects.equals(getAttributes(), that.getAttributes())
225                                && Objects.equals(getTransactionId(), that.getTransactionId())
226                                && Objects.equals(getMediaType(), that.getMediaType())
227                                && Objects.equals(getPayloadMessageKey(), that.getPayloadMessageKey());
228        }
229
230        @Override
231        public int hashCode() {
232                return Objects.hash(getOperationType(), getAttributes(), getTransactionId(), getMediaType());
233        }
234}