View Javadoc
1   /*
2    * Copyright 2016 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.openehealth.ipf.commons.ihe.hl7v2;
17  
18  import ca.uhn.hl7v2.ErrorCode;
19  import ca.uhn.hl7v2.HL7Exception;
20  import ca.uhn.hl7v2.HapiContext;
21  import ca.uhn.hl7v2.Version;
22  import ca.uhn.hl7v2.model.Message;
23  import ca.uhn.hl7v2.model.Segment;
24  import ca.uhn.hl7v2.parser.Parser;
25  import ca.uhn.hl7v2.util.Terser;
26  import lombok.Getter;
27  import org.apache.commons.lang3.ArrayUtils;
28  import org.apache.commons.lang3.StringUtils;
29  import org.openehealth.ipf.commons.ihe.core.TransactionConfiguration;
30  import org.openehealth.ipf.commons.ihe.core.atna.AuditStrategy;
31  import org.openehealth.ipf.commons.ihe.hl7v2.audit.MllpAuditDataset;
32  import org.openehealth.ipf.modules.hl7.HL7v2Exception;
33  import org.openehealth.ipf.modules.hl7.message.MessageUtils;
34  
35  import java.util.*;
36  
37  import static org.apache.commons.lang3.Validate.*;
38  
39  /**
40   * Endpoint-agnostic parameters of an HL7v2-based transaction.
41   *
42   * @author Dmytro Rud
43   */
44  public class Hl7v2TransactionConfiguration<T extends MllpAuditDataset> extends TransactionConfiguration<T> {
45  
46      private static class Definition {
47          private final Set<String> triggerEvents;
48          private final boolean auditable;
49          private final boolean responseContinuable;
50  
51          Definition(String triggerEventsString, boolean auditable, boolean responseContinuable) {
52              this.triggerEvents = new HashSet<>(Arrays.asList(StringUtils.split(triggerEventsString, ' ')));
53              this.auditable = auditable;
54              this.responseContinuable = responseContinuable;
55          }
56          
57          boolean isAllowedTriggerEvent(String triggerEvent) {
58              return triggerEvents.contains("*") || triggerEvents.contains(triggerEvent);
59          }
60      }
61  
62  
63      @Getter private final String sendingApplication;
64      @Getter private final String sendingFacility;
65  
66      @Getter private final ErrorCode requestErrorDefaultErrorCode;
67      @Getter private final ErrorCode responseErrorDefaultErrorCode;
68  
69      @Getter private final HapiContext hapiContext;
70      @Getter private final String[] allowedRequestMessageTypes;
71      @Getter private final String[] allowedRequestTriggerEvents;
72      @Getter private final String[] allowedResponseMessageTypes;
73      @Getter private final String[] allowedResponseTriggerEvents;
74      @Getter private final Version[] hl7Versions;
75  
76      // true = request, false = response
77      private final Map<Boolean, Map<String, Definition>> definitions;
78  
79  
80      /**
81       * Constructor.
82       *
83       * @param hl7Versions                   HL7 versions for acceptance checks (MSH-12). The first version of this array is used for default NAKs.
84       * @param sendingApplication            sending application for default NAKs (MSH-3).
85       * @param sendingFacility               sending application for default NAKs (MSH-4).
86       * @param requestErrorDefaultErrorCode  default error code for request-related NAKs.
87       * @param responseErrorDefaultErrorCode default error code for response-related NAKs.
88       * @param allowedRequestMessageTypes    array of allowed request message types,
89       *                                      e.g. <code>{"ADT", "MDM"}</code>.
90       * @param allowedRequestTriggerEvents   array of allowed request trigger events
91       *                                      for each request message type,
92       *                                      e.g. <code>{"A01 A02 A03", "T06 T07 T08"}</code>.
93       * @param allowedResponseMessageTypes   array of allowed response message types, e.g. <code>{"ACK", "RSP"}</code>.
94       * @param allowedResponseTriggerEvents  array of allowed response trigger events for each message type,
95       *                                      ignored for messages of type "ACK".
96       * @param auditabilityFlags             flags of whether the messages of corresponding
97       *                                      message type should be audited.
98       *                                      If <code>null</code>, the transaction will not perform any auditing.
99       * @param responseContinuabilityFlags   flags of whether the messages of corresponding
100      *                                      message type should support HL7 response continuations.
101      *                                      If <code>null</code>, no continuations will be supported.
102      * @param hapiContext                   transaction-specific HAPI Context
103      */
104     public Hl7v2TransactionConfiguration(
105             String name,
106             String description,
107             boolean isQuery,
108             AuditStrategy<T> clientAuditStrategy,
109             AuditStrategy<T> serverAuditStrategy,
110             Version[] hl7Versions,
111             String sendingApplication,
112             String sendingFacility,
113             ErrorCode requestErrorDefaultErrorCode,
114             ErrorCode responseErrorDefaultErrorCode,
115             String[] allowedRequestMessageTypes,
116             String[] allowedRequestTriggerEvents,
117             String[] allowedResponseMessageTypes,
118             String[] allowedResponseTriggerEvents,
119             boolean[] auditabilityFlags,
120             boolean[] responseContinuabilityFlags,
121             HapiContext hapiContext)
122     {
123         super(name, description, isQuery, clientAuditStrategy, serverAuditStrategy);
124 
125         notNull(hl7Versions);
126         notNull(sendingApplication);
127         notNull(sendingFacility);
128 
129         noNullElements(allowedRequestMessageTypes);
130         noNullElements(allowedRequestTriggerEvents);
131         noNullElements(allowedResponseMessageTypes);
132         noNullElements(allowedResponseTriggerEvents);
133         notNull(hapiContext);
134 
135         notEmpty(allowedRequestMessageTypes);
136         isTrue(allowedRequestMessageTypes.length == allowedRequestTriggerEvents.length);
137         isTrue(allowedRequestMessageTypes.length == allowedResponseMessageTypes.length);
138         isTrue(allowedRequestMessageTypes.length == allowedResponseTriggerEvents.length);
139         if (auditabilityFlags != null) {
140             isTrue(allowedRequestMessageTypes.length == auditabilityFlags.length);
141         }
142         if (responseContinuabilityFlags != null) {
143             isTrue(allowedRequestMessageTypes.length == responseContinuabilityFlags.length);
144         }
145 
146         // QC passed ;)
147 
148         this.hl7Versions = hl7Versions;
149         this.allowedRequestMessageTypes = allowedRequestMessageTypes;
150         this.allowedRequestTriggerEvents = allowedRequestTriggerEvents;
151         this.allowedResponseMessageTypes = allowedResponseMessageTypes;
152         this.allowedResponseTriggerEvents = allowedResponseTriggerEvents;
153         this.sendingApplication = sendingApplication;
154         this.sendingFacility = sendingFacility;
155 
156         this.requestErrorDefaultErrorCode = requestErrorDefaultErrorCode;
157         this.responseErrorDefaultErrorCode = responseErrorDefaultErrorCode;
158 
159         this.hapiContext = hapiContext;
160 
161         this.definitions = new HashMap<>();
162         definitions.put(true, createDefinitionsMap(allowedRequestMessageTypes, allowedRequestTriggerEvents,
163                 auditabilityFlags, responseContinuabilityFlags));
164         definitions.put(false, createDefinitionsMap(allowedResponseMessageTypes, allowedResponseTriggerEvents,
165                 auditabilityFlags, responseContinuabilityFlags));
166 
167         if (! definitions.get(false).containsKey("ACK")) {
168             definitions.get(false).put("ACK", new Definition("*", false, false));
169         }
170     }
171 
172     public Hl7v2TransactionConfiguration(
173             String name,
174             String description,
175             boolean isQuery,
176             AuditStrategy<T> clientAuditStrategy,
177             AuditStrategy<T> serverAuditStrategy,
178             Version hl7Version,
179             String sendingApplication,
180             String sendingFacility,
181             ErrorCode requestErrorDefaultErrorCode,
182             ErrorCode responseErrorDefaultErrorCode,
183             String allowedRequestMessageType,
184             String allowedRequestTriggerEvent,
185             String allowedResponseMessageType,
186             String allowedResponseTriggerEvent,
187             boolean auditabilityFlag,
188             boolean responseContinuabilityFlag,
189             HapiContext hapiContext)
190     {
191         this(name, description, isQuery, clientAuditStrategy, serverAuditStrategy,
192                 new Version[]{hl7Version}, sendingApplication, sendingFacility, requestErrorDefaultErrorCode, responseErrorDefaultErrorCode,
193                 new String[]{allowedRequestMessageType}, new String[]{allowedRequestTriggerEvent},
194                 new String[]{allowedResponseMessageType}, new String[]{allowedResponseTriggerEvent},
195                 new boolean[]{auditabilityFlag},
196                 new boolean[]{responseContinuabilityFlag},
197                 hapiContext);
198     }
199 
200     private static Map<String, Definition> createDefinitionsMap(
201             String[] allowedMessageTypes,
202             String[] allowedTriggerEvents,
203             boolean[] auditabilityFlags,
204             boolean[] responseContinuabilityFlags)
205     {
206         Map<String, Definition> result = new HashMap<>();
207         for (int i = 0; i < allowedMessageTypes.length; ++i) {
208             Definition definition = new Definition(
209                     allowedTriggerEvents[i],
210                     (auditabilityFlags != null) && auditabilityFlags[i],
211                     (responseContinuabilityFlags != null) && responseContinuabilityFlags[i]);
212             result.put(allowedMessageTypes[i], definition);
213         }
214         return result;
215     }
216 
217 
218     /**
219      * Returns <code>true</code> when request messages of the given type are auditable.
220      */
221     public boolean isAuditable(String messageType) {
222         try {
223             Definition definition = getDefinitionForMessageType(messageType, true);
224             return definition.auditable;
225         } catch (Hl7v2AcceptanceException e) {
226             throw new IllegalArgumentException(e);
227         }
228 
229     }
230 
231 
232     /**
233      * Returns <code>true</code> when request messages of the given type
234      * can be split by means of interactive continuation.
235      * <p/>
236      * When this method returns true, the request message structure must
237      * be able to contain segments RCP, QPD, DSC; and the response message
238      * structure -- segments DSC, QAK.
239      */
240     public boolean isContinuable(String messageType) {
241         try {
242             Definition definition = getDefinitionForMessageType(messageType, true);
243             return definition.responseContinuable;
244         } catch (Hl7v2AcceptanceException e) {
245             throw new IllegalArgumentException(e);
246         }
247     }
248 
249 
250     /**
251      * Returns <code>true</code> if the given element of the given list
252      * contains a start segment of a data record.
253      */
254     public boolean isDataStartSegment(List<String> segments, int index) {
255         return false;
256     }
257 
258 
259     /**
260      * Returns <code>true</code> if the given element of the given list
261      * contains a segment which belongs to segments following the data
262      * records ("footer").
263      */
264     public boolean isFooterStartSegment(List<String> segments, int index) {
265         return false;
266     }
267 
268 
269     /**
270      * Performs transaction-specific acceptance test of the given request message.
271      *
272      * @param message {@link Message} object.
273      */
274     public void checkRequestAcceptance(Message message) throws Hl7v2AcceptanceException {
275         checkMessageAcceptance(message, true);
276     }
277 
278 
279     /**
280      * Performs transaction-specific acceptance test of the given response message.
281      *
282      * @param message {@link Message} object.
283      */
284     public void checkResponseAcceptance(Message message) throws Hl7v2AcceptanceException {
285         checkMessageAcceptance(message, false);
286 
287         try {
288             if (!ArrayUtils.contains(
289                     new String[]{"AA", "AR", "AE", "CA", "CR", "CE"},
290                     new Terser(message).get("MSA-1")))
291             {
292                 throw new Hl7v2AcceptanceException("Bad response: missing or invalid MSA segment", ErrorCode.REQUIRED_FIELD_MISSING);
293             }
294         } catch (HL7Exception e) {
295             throw new Hl7v2AcceptanceException("Bad response: missing or invalid MSA segment", ErrorCode.APPLICATION_INTERNAL_ERROR);
296         }
297     }
298 
299 
300     /**
301      * Performs acceptance test of the given message.
302      *
303      * @param message   HAPI {@link Message} object.
304      * @param isRequest <code>true</code> iff the message is a request.
305      * @throws Hl7v2AcceptanceException when the message is not acceptable.
306      */
307     public void checkMessageAcceptance(
308             Message message,
309             boolean isRequest) throws Hl7v2AcceptanceException {
310         try {
311             Segment msh = (Segment) message.get("MSH");
312             checkMessageAcceptance(
313                     Terser.get(msh, 9, 0, 1, 1),
314                     Terser.get(msh, 9, 0, 2, 1),
315                     Terser.get(msh, 9, 0, 3, 1),
316                     Terser.get(msh, 12, 0, 1, 1),
317                     isRequest);
318         } catch (Hl7v2AcceptanceException e) {
319             throw e;
320         } catch (HL7Exception e) {
321             throw new Hl7v2AcceptanceException("Missing or invalid MSH segment: " + e.getMessage(), ErrorCode.APPLICATION_INTERNAL_ERROR);
322         }
323     }
324 
325 
326     /**
327      * Performs acceptance test of the message with the given attributes.
328      *
329      * @param messageType      value from MSH-9-1, can be empty or <code>null</code>.
330      * @param triggerEvent     value from MSH-9-2, can be empty or <code>null</code>.
331      * @param messageStructure value from MSH-9-3, can be empty or <code>null</code>.
332      * @param version          value from MSH-12, can be empty or <code>null</code>.
333      * @param isRequest        <code>true</code> iff the message under consideration is a request.
334      * @throws Hl7v2AcceptanceException when the message is not acceptable.
335      */
336     public void checkMessageAcceptance(
337             String messageType,
338             String triggerEvent,
339             String messageStructure,
340             String version,
341             boolean isRequest) throws Hl7v2AcceptanceException
342     {
343         checkMessageVersion(version);
344 
345         Definition definition = getDefinitionForMessageType(messageType, isRequest);
346 
347         if (! definition.isAllowedTriggerEvent(triggerEvent)) {
348             throw new Hl7v2AcceptanceException("Invalid trigger event " + triggerEvent + ", must be one of " +
349                     join(definition.triggerEvents), ErrorCode.UNSUPPORTED_EVENT_CODE);
350         }
351 
352         if (!StringUtils.isEmpty(messageStructure)) {
353             // This may not work as the custom event map cannot be distinguished from the
354             // default one! This needs to be fixed for HAPI 2.1
355             String event = messageType + "_" + triggerEvent;
356             String expectedMessageStructure;
357             try {
358                 expectedMessageStructure = hapiContext.getModelClassFactory().getMessageStructureForEvent(event, Version.versionOf(version));
359             } catch (HL7Exception e) {
360                 throw new Hl7v2AcceptanceException("Acceptance check failed", ErrorCode.UNKNOWN_KEY_IDENTIFIER);
361             }
362 
363             // TODO when upgrading to HAPI 2.1 remove the constant IF statements
364             if ("QBP_ZV1".equals(event)) {
365                 expectedMessageStructure = "QBP_Q21";
366             } else if ("RSP_ZV2".equals(event)) {
367                 expectedMessageStructure = "RSP_ZV2";
368             }
369 
370             // the expected structure must be equal to the actual one,
371             // but second components may be omitted in acknowledgements
372             boolean bothAreEqual = messageStructure.equals(expectedMessageStructure);
373             boolean bothAreAcks = (messageStructure.startsWith("ACK") && expectedMessageStructure != null && expectedMessageStructure.startsWith("ACK"));
374             if (!(bothAreEqual || bothAreAcks)) {
375                 throw new Hl7v2AcceptanceException("Invalid message structure " + messageStructure + 
376                         ", must be " + expectedMessageStructure, ErrorCode.APPLICATION_INTERNAL_ERROR);
377             }
378         }
379     }
380 
381     private Definition getDefinitionForMessageType(String messageType, boolean isRequest) throws Hl7v2AcceptanceException {
382         Definition definition = definitions.get(isRequest).get(messageType);
383         if (definition == null) {
384             definition = definitions.get(isRequest).get("*");
385             if (definition == null) {
386                 throw new Hl7v2AcceptanceException("Invalid message type " + messageType + ", must be one of " +
387                         join(definitions.get(isRequest).keySet()), ErrorCode.UNSUPPORTED_MESSAGE_TYPE);
388             }
389         }
390         return definition;
391     }
392 
393     private void checkMessageVersion(String version) throws Hl7v2AcceptanceException {
394         Version messageVersion = Version.versionOf(version);
395         if (! ArrayUtils.contains(hl7Versions, messageVersion)) {
396             throw new Hl7v2AcceptanceException("Invalid HL7 version " + version + ", must be one of " + supportedVersions(hl7Versions),
397                     ErrorCode.UNSUPPORTED_VERSION_ID);
398         }
399     }
400 
401     private String supportedVersions(Version... hl7versions) {
402         StringBuilder builder = new StringBuilder();
403         for (Version v : hl7versions) {
404             builder.append(v.getVersion()).append(' ');
405         }
406         return builder.toString();
407     }
408 
409     public Parser getParser() {
410         return getHapiContext().getPipeParser();
411     }
412 
413     private static String join(Collection<String> collection) {
414         return StringUtils.join(collection, ' ');
415     }
416 
417     /**
418      * Makes a valid request for this transaction. Note that the individual transaction types
419      * may overload this method, e.g. using a concrete response type.
420      *
421      * @param messageType message type, e.g. ADT
422      * @param trigger trigger event, e.g. A01
423      * @return HAPI message created using the correct HapiContext
424      * @throws HL7v2Exception if the message type or trigger event is not valid for this transaction
425      */
426     public <M extends Message> M request(String messageType, String trigger) {
427         Message message = MessageUtils.makeMessage(
428                 getHapiContext(), messageType, trigger, getHl7Versions()[0].getVersion());
429         try {
430             checkRequestAcceptance(message);
431             return (M) message;
432         } catch (Hl7v2AcceptanceException e) {
433             throw new HL7v2Exception(e);
434         }
435     }
436 
437     /**
438      * Like {@link #request(String, String)}, but uses the first configured request message type as default.
439      */
440     public <M extends Message> M request(String trigger) {
441         return request(allowedRequestMessageTypes[0], trigger);
442     }
443 
444     /**
445      * Like {@link #request(String, String)}, but uses the first configured request message type
446      * and the first configured trigger event as defaults.
447      */
448     public <M extends Message> M request() {
449         return request(allowedRequestMessageTypes[0], allowedRequestTriggerEvents[0]);
450     }
451 }
452