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.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
037import static org.apache.commons.lang3.StringUtils.defaultString;
038
039@SuppressWarnings("WeakerAccess")
040public abstract class BaseResourceMessage implements IResourceMessage, IModelJson {
041
042        @JsonProperty("operationType")
043        protected BaseResourceModifiedMessage.OperationTypeEnum myOperationType;
044
045        @JsonProperty("attributes")
046        private Map<String, String> myAttributes;
047
048        @JsonProperty("transactionId")
049        private String myTransactionId;
050
051        @JsonProperty("mediaType")
052        private String myMediaType;
053
054        /**
055         * This is used by any message going to kafka for topic partition selection purposes.
056         */
057        @JsonProperty("messageKey")
058        private String myMessageKey;
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                Validate.notNull(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        @Deprecated
171        @Nullable
172        public String getMessageKeyOrNull() {
173                return getMessageKey();
174        }
175
176        @Nullable
177        public String getMessageKey() {
178                return myMessageKey;
179        }
180
181        public void setMessageKey(String theMessageKey) {
182                myMessageKey = theMessageKey;
183        }
184
185        /**
186         * Returns {@link #getMessageKey()} or {@link #getMessageKeyDefaultValue()} when {@link #getMessageKey()} returns <code>null</code>.
187         *
188         * @return the message key value or default
189         */
190        @Nullable
191        public String getMessageKeyOrDefault() {
192                return defaultString(getMessageKey(), getMessageKeyDefaultValue());
193        }
194
195        /**
196         * Provides a fallback value when method {@link #getMessageKey()} returns <code>null</code>.
197         *
198         * @return null by default
199         */
200        @Nullable
201        protected String getMessageKeyDefaultValue() {
202                return null;
203        }
204
205        public enum OperationTypeEnum {
206                CREATE(RestOperationTypeEnum.CREATE),
207                UPDATE(RestOperationTypeEnum.UPDATE),
208                DELETE(RestOperationTypeEnum.DELETE),
209                MANUALLY_TRIGGERED(RestOperationTypeEnum.UPDATE),
210                TRANSACTION(RestOperationTypeEnum.UPDATE);
211
212                private final RestOperationTypeEnum myRestOperationTypeEnum;
213
214                OperationTypeEnum(RestOperationTypeEnum theRestOperationTypeEnum) {
215                        myRestOperationTypeEnum = theRestOperationTypeEnum;
216                }
217
218                public static OperationTypeEnum from(RestOperationTypeEnum theRestOperationType) {
219                        switch (theRestOperationType) {
220                                case CREATE:
221                                        return CREATE;
222                                case UPDATE:
223                                        return UPDATE;
224                                case DELETE:
225                                        return DELETE;
226                                default:
227                                        throw new IllegalArgumentException(
228                                                        Msg.code(2348) + "Unsupported operation type: " + theRestOperationType);
229                        }
230                }
231
232                public RestOperationTypeEnum asRestOperationType() {
233                        return myRestOperationTypeEnum;
234                }
235        }
236
237        @VisibleForTesting
238        public Map<String, String> getAttributes() {
239                return ObjectUtils.defaultIfNull(myAttributes, Collections.emptyMap());
240        }
241
242        @Override
243        public boolean equals(Object theO) {
244                if (this == theO) return true;
245                if (theO == null || getClass() != theO.getClass()) return false;
246                BaseResourceMessage that = (BaseResourceMessage) theO;
247                return getOperationType() == that.getOperationType()
248                                && Objects.equals(getAttributes(), that.getAttributes())
249                                && Objects.equals(getTransactionId(), that.getTransactionId())
250                                && Objects.equals(getMediaType(), that.getMediaType())
251                                && Objects.equals(getMessageKey(), that.getMessageKey());
252        }
253
254        @Override
255        public int hashCode() {
256                return Objects.hash(getOperationType(), getAttributes(), getTransactionId(), getMediaType());
257        }
258}