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