001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2026 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.api.pub.hl7v2.model;
011
012import ca.cdr.api.model.json.IModelJson;
013import ca.uhn.hl7v2.HL7Exception;
014import ca.uhn.hl7v2.model.AbstractMessage;
015import ca.uhn.hl7v2.model.Message;
016import com.fasterxml.jackson.annotation.JsonCreator;
017import com.fasterxml.jackson.annotation.JsonIgnore;
018import com.fasterxml.jackson.annotation.JsonProperty;
019import com.fasterxml.jackson.annotation.JsonPropertyOrder;
020import io.swagger.v3.oas.annotations.media.Schema;
021import jakarta.annotation.Nonnull;
022import org.hl7.fhir.instance.model.api.IBaseBundle;
023import org.springframework.util.CollectionUtils;
024
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.BUNDLES;
031import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.DO_AUTO_CONVERT;
032import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.DO_PROCESS;
033import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.ISSUES;
034import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.MODIFIABLE_MESSAGE;
035import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.ORIGINAL_MESSAGE;
036import static ca.cdr.api.pub.hl7v2.model.Hl7v2ToFhirConversionResultJson.REQUEST_HEADERS;
037import static java.util.Objects.isNull;
038
039/**
040 * Contains all relevant data involved in the conversion of an HL7 v2.x message to a list of IBaseBundle resources.
041 */
042@JsonPropertyOrder({ORIGINAL_MESSAGE, MODIFIABLE_MESSAGE, BUNDLES, ISSUES, DO_PROCESS, DO_AUTO_CONVERT, REQUEST_HEADERS
043})
044public class Hl7v2ToFhirConversionResultJson implements IModelJson {
045
046        public static final String ORIGINAL_MESSAGE = "originalMessage";
047        public static final String MODIFIABLE_MESSAGE = "modifiableMessage";
048        public static final String BUNDLES = "bundles";
049        public static final String ISSUES = "issues";
050        public static final String DO_PROCESS = "doProcess";
051        public static final String DO_AUTO_CONVERT = "doAutoConvert";
052        public static final String REQUEST_HEADERS = "requestHeaders";
053
054        /**
055         * The original HL7 v2.x message before any customizations have been applied.
056         */
057        @JsonProperty(ORIGINAL_MESSAGE)
058        private Message myOriginalMessage;
059
060        /**
061         * Cached clone of {@link #myOriginalMessage}, populated once in the constructor and returned
062         * by {@link #getOriginalMessage()} so we don't pay a deep-clone cost on every read.
063         */
064        @JsonIgnore
065        private Message myOriginalMessageClone;
066
067        /**
068         * The HL7 v2.x message which customizations can be applied to.
069         */
070        @JsonProperty(MODIFIABLE_MESSAGE)
071        private Message myModifiableMessage;
072
073        /**
074         * A list of Bundle resources that have been created from the modifiableMessage.
075         */
076        @JsonProperty(BUNDLES)
077        private List<IBaseBundle> myBundles = new ArrayList<>();
078
079        /**
080         * A list of issues that have occurred throughout the conversion process.
081         */
082        @JsonProperty(ISSUES)
083        private List<MappingMessage> myIssues = new ArrayList<>();
084
085        /**
086         * A flag to indicate whether a given message should be processed.
087         */
088        @JsonProperty(DO_PROCESS)
089        private boolean myDoProcess;
090
091        /**
092         * A flag to indicate whether a given message should be passed through the Smile generic mapper.
093         * Set this to false in order to skip the Smile generic mapper entirely.
094         */
095        @JsonProperty(DO_AUTO_CONVERT)
096        private boolean myDoAutoConvert;
097
098        @JsonProperty(REQUEST_HEADERS)
099        @Schema(description = "The HTTP request headers associated with the message transmission when applicable")
100        private Map<String, String> myRequestHeaders;
101
102        /**
103         * Constructor
104         * @param theOriginalMessage    The original message to be preserved during the HL7V2 to FHIR conversion
105         */
106        @JsonCreator
107        public Hl7v2ToFhirConversionResultJson(@JsonProperty(ORIGINAL_MESSAGE) Message theOriginalMessage)
108                        throws HL7Exception {
109                myOriginalMessage = theOriginalMessage;
110                myOriginalMessageClone = cloneMessage(theOriginalMessage);
111                myModifiableMessage = cloneMessage(theOriginalMessage);
112                myDoProcess = true;
113                myDoAutoConvert = true;
114        }
115
116        /**
117         * @deprecated  Use {@link AbstractMessage#copy()} instead.
118         * <br/><br/>
119         * Clones the specified {@link Message} into a new {@link Message} with the same contents
120         * @param theMessage            The message to clone
121         */
122        @Deprecated(since = "2023.11.R01")
123        public static Message cloneMessage(Message theMessage) throws HL7Exception {
124                return ((AbstractMessage) theMessage).copy();
125        }
126
127        /**
128         * Adds a bundle
129         * @param theBundle The bundle to add
130         */
131        public void addBundle(IBaseBundle theBundle) {
132                myBundles.add(theBundle);
133        }
134
135        /**
136         * Adds a message to the conversion result. Acceptable message levels are `INFO`, `WARNING`, and `ERROR`
137         *
138         * @param thePath The path within the message where the issue was detected
139         * @param theMessageLevel The issue error level, e.g. 'INFO', 'WARNING', or 'ERROR'.
140         * @param theIssue The description of the issue
141         */
142        public void addIssue(String thePath, MappingMessage.MessageLevel theMessageLevel, String theIssue) {
143                MappingMessage issue = new MappingMessage(thePath, MappingMessage.PathType.HL7V2, theMessageLevel, theIssue);
144                this.addIssue(issue);
145        }
146
147        /**
148         * Adds an issue
149         * @param theIssue      The issue to add
150         */
151        public void addIssue(MappingMessage theIssue) {
152                if (myIssues.isEmpty()) {
153                        myIssues = new ArrayList<>();
154                }
155                myIssues.add(theIssue);
156        }
157
158        /**
159         * Indicates whether there were any issues ({@link MappingMessage.MessageLevel#INFO},
160         * {@link MappingMessage.MessageLevel#WARNING}, or {@link MappingMessage.MessageLevel#ERROR})
161         * during the message conversion
162         * @return true if there are any issues
163         */
164        public boolean hasIssues() {
165                return !CollectionUtils.isEmpty(myIssues);
166        }
167
168        /**
169         * Indicates whether there were any {@link MappingMessage.MessageLevel#INFO} issues
170         * during the message conversion
171         * @return true if there are any {@link MappingMessage.MessageLevel#INFO} issues
172         */
173        public boolean hasInfoIssues() {
174                return hasIssueOfType(MappingMessage.MessageLevel.INFO);
175        }
176
177        /**
178         * Indicates whether there were any {@link MappingMessage.MessageLevel#WARNING} issues
179         * during the message conversion
180         * @return true if there are any {@link MappingMessage.MessageLevel#WARNING} issues
181         */
182        public boolean hasWarningIssues() {
183                return hasIssueOfType(MappingMessage.MessageLevel.WARNING);
184        }
185
186        /**
187         * Indicates whether there were any {@link MappingMessage.MessageLevel#ERROR} issues
188         * during the message conversion
189         * @return true if there are any {@link MappingMessage.MessageLevel#ERROR} issues
190         */
191        public boolean hasErrorIssues() {
192                return hasIssueOfType(MappingMessage.MessageLevel.ERROR);
193        }
194
195        private boolean hasIssueOfType(MappingMessage.MessageLevel theType) {
196                return myIssues.stream().anyMatch(m -> theType.equals(m.getLevel()));
197        }
198
199        /**
200         * Returns a cached clone of the original HL7 v2.x message before any customizations have been
201         * applied. The same clone instance is returned on every call, so callers must treat the returned
202         * Message as read-only ? use {@link #getModifiableMessage()} if you need to mutate.
203         *
204         * @return a cached clone of the original HL7 v2.x message
205         */
206        public Message getOriginalMessage() throws HL7Exception {
207                return myOriginalMessageClone;
208        }
209
210        /**
211         * Returns the raw original HL7 v2.x message reference held by this result, with no defensive
212         * copy. The method name encodes the contract: callers MUST NOT modify the returned Message.
213         * Prefer this over {@link #getOriginalMessage()} on hot paths where the returned Message is
214         * only inspected (e.g. {@code .encode()}, {@code .printStructure()}, read-only {@code Terser}).
215         *
216         * @return the raw, shared original HL7 v2.x message ? must not be modified
217         */
218        public Message getOriginalMessageDoNotModify() {
219                return myOriginalMessage;
220        }
221
222        /**
223         * @return              The HL7 v2.x message which customizations can be applied to.
224         */
225        public Message getModifiableMessage() {
226                return myModifiableMessage;
227        }
228
229        /**
230         * Sets the HL7 v2.x message which customizations can be applied to.
231         * @param theModifiableMessage          The modifiable Message
232         */
233        public void setModifiableMessage(Message theModifiableMessage) {
234                myModifiableMessage = theModifiableMessage;
235        }
236
237        /**
238         * @return A list of Bundle resources that have been created from the modifiable Message.
239         */
240        public List<IBaseBundle> getBundles() {
241                return myBundles;
242        }
243
244        /**
245         * Sets the list of Bundle resources that have been created from the modifiable Message.
246         * @param theBundles                            The Bundles
247         */
248        public void setBundles(List<IBaseBundle> theBundles) {
249                myBundles = theBundles;
250        }
251
252        /**
253         * @return A list of issues that have occurred throughout the conversion process.
254         */
255        public List<MappingMessage> getIssues() {
256                return myIssues;
257        }
258
259        /**
260         * Sets the list of issues that have occurred throughout the conversion process.
261         * @param theIssues             The issues
262         */
263        public void setIssues(List<MappingMessage> theIssues) {
264                myIssues = new ArrayList<>();
265                addIssues(theIssues);
266        }
267
268        /**
269         * Adds the items in list of issues that have occurred throughout the conversion process.
270         * @param theIssues             The issues
271         */
272        public void addIssues(List<MappingMessage> theIssues) {
273                if (!theIssues.isEmpty()) {
274                        myIssues.addAll(theIssues);
275                }
276        }
277
278        /**
279         * @return              Whether the modifiable Message should be processed.
280         */
281        public boolean isDoProcess() {
282                return myDoProcess;
283        }
284
285        /**
286         * Sets a flag to indicate whether the modifiable Message should be processed.
287         * @param theDoProcess          The value of the flag
288         */
289        public void setDoProcess(boolean theDoProcess) {
290                myDoProcess = theDoProcess;
291        }
292
293        /**
294         * @return Whether the modifiable Message should be passed through the Smile generic mapper.
295         */
296        public boolean isDoAutoConvert() {
297                return myDoAutoConvert;
298        }
299
300        /**
301         * Sets a flag to indicate whether the modifiable Message should be passed through the Smile generic mapper.
302         * <br/>
303         * The Smile generic mapper will be skipped entirely if this flag is set to false.
304         * @param theDoAutoConvert              The value of the flag
305         */
306        public void setDoAutoConvert(boolean theDoAutoConvert) {
307                myDoAutoConvert = theDoAutoConvert;
308        }
309
310        /**
311         *
312         * @return the http request headers associated with the request (if any).
313         */
314        @Nonnull
315        public Map<String, String> getRequestHeaders() {
316                if (isNull(myRequestHeaders)) {
317                        myRequestHeaders = new HashMap<>();
318                }
319
320                return myRequestHeaders;
321        }
322
323        /**
324         * Stores the request headers associated with the request (if any)
325         * @param theRequestHeaders
326         */
327        public void setRequestHeaders(Map<String, String> theRequestHeaders) {
328                myRequestHeaders = theRequestHeaders;
329        }
330}