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.AcknowledgmentCode;
19  import ca.uhn.hl7v2.HL7Exception;
20  import ca.uhn.hl7v2.model.Message;
21  import org.openehealth.ipf.commons.ihe.hl7v2.audit.MllpAuditDataset;
22  import org.openehealth.ipf.modules.hl7.message.MessageUtils;
23  
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.stream.Stream;
30  
31  import static java.util.Objects.requireNonNull;
32  
33  
34  /**
35   * Basic ACK and NAK factory for HL7v2-based transactions.
36   *
37   * @author Dmytro Rud
38   */
39  public class NakFactory<T extends MllpAuditDataset> {
40  
41      private final Hl7v2TransactionConfiguration<T> config;
42      private final boolean useCAckTypeCodes;
43      private final String defaultNakMsh9;
44  
45  
46      /**
47       * Generic constructor.
48       *
49       * @param config           Configuration of the transaction served by this factory.
50       * @param useCAckTypeCodes if <code>true</code>, HL7v2 acknowledgement codes
51       *                         <tt>CA</tt>, <tt>CE</tt>, <tt>CR</tt> will be used instead of the default
52       *                         <tt>AA</tt>, <tt>AE</tt>, <tt>AR</tt>.
53       * @param defaultNakMsh9   desired contents of MSH-9 in this transaction's default NAKs.
54       */
55      public NakFactory(Hl7v2TransactionConfiguration<T> config, boolean useCAckTypeCodes, String defaultNakMsh9) {
56          this.config = requireNonNull(config);
57          this.useCAckTypeCodes = useCAckTypeCodes;
58          this.defaultNakMsh9 = requireNonNull(defaultNakMsh9);
59      }
60  
61  
62      /**
63       * Short constructor which corresponds to <code>NakFactory(config, false, "ACK")</code>.
64       *
65       * @param config Configuration of the transaction served by this factory.
66       */
67      public NakFactory(Hl7v2TransactionConfiguration<T> config) {
68          this(config, false, "ACK");
69      }
70  
71  
72      /**
73       * Generates a transaction-specific HL7v2 ACK response message on the basis
74       * of the original HAPI request message.
75       *
76       * @param originalMessage original HAPI request message.
77       */
78      public Message createAck(Message originalMessage) throws HL7Exception, IOException {
79          return originalMessage.generateACK(useCAckTypeCodes ? AcknowledgmentCode.CA : AcknowledgmentCode.AA, null);
80      }
81  
82  
83      /**
84       * Generates an HL7v2 NAK response message on the basis
85       * of the thrown exception and the original HAPI request message.
86       *
87       * @param exception       thrown exception.
88       * @param originalMessage original HAPI request message.
89       * @param ackTypeCode     HL7v2 acknowledgement type code.
90       */
91      public Message createNak(Message originalMessage, HL7Exception exception, AcknowledgmentCode ackTypeCode) throws HL7Exception, IOException {
92          return originalMessage.generateACK(ackTypeCode, exception);
93      }
94  
95  
96      /**
97       * Generates an HL7v2 NAK response message on the basis
98       * of the thrown exception and the original HAPI request message.
99       *
100      * @param t               thrown exception.
101      * @param originalMessage original HAPI request message.
102      */
103     public Message createNak(Message originalMessage, Throwable t) throws HL7Exception, IOException {
104         HL7Exception hl7Exception = getHl7Exception(t);
105         return createNak(originalMessage, hl7Exception, getAckTypeCode(hl7Exception));
106     }
107 
108 
109     /**
110      * Generates a "default" HL7v2 NAK message on the basis
111      * of the thrown exception.
112      *
113      * @param e thrown exception.
114      */
115     public Message createDefaultNak(HL7Exception e) {
116         HL7Exception hl7Exception = new HL7Exception(
117                 formatErrorMessage(e),
118                 config.getRequestErrorDefaultErrorCode(),
119                 e);
120 
121         return MessageUtils.defaultNak(
122                 hl7Exception,
123                 useCAckTypeCodes ? AcknowledgmentCode.CR : AcknowledgmentCode.AR,
124                 config.getHl7Versions()[0].getVersion(),
125                 config.getSendingApplication(),
126                 config.getSendingFacility(),
127                 defaultNakMsh9);
128     }
129 
130 
131     /**
132      * Returns a HL7v2 exception that corresponds
133      * to the given instance of {@link Throwable}.
134      */
135     protected HL7Exception getHl7Exception(Throwable t) {
136         return getException(HL7Exception.class, t).orElseGet(() ->
137                 new HL7Exception(
138                         formatErrorMessage(t),
139                         config.getRequestErrorDefaultErrorCode(),
140                         t));
141     }
142 
143 
144     /**
145      * Returns a HL7v2 acknowledgement type code that corresponds
146      * to the given instance of {@link Throwable}.
147      */
148     protected AcknowledgmentCode getAckTypeCode(Throwable t) {
149         AcknowledgmentCode ackTypeCode = (t instanceof Hl7v2AcceptanceException) ? AcknowledgmentCode.AR : AcknowledgmentCode.AE;
150         if (useCAckTypeCodes) {
151             ackTypeCode = (ackTypeCode == AcknowledgmentCode.AR) ? AcknowledgmentCode.CR : AcknowledgmentCode.CE;
152         }
153         return ackTypeCode;
154     }
155 
156 
157     /**
158      * Formats and returns error message of an exception.
159      * <p>
160      * In particular, all line break characters must be removed,
161      * otherwise they will break the structure of an HL7 NAK.
162      *
163      * @param t thrown exception.
164      * @return formatted error message from the given exception.
165      */
166     public static String formatErrorMessage(Throwable t) {
167         String s = t.getMessage();
168         if (s == null) {
169             s = t.getClass().getName();
170         }
171         s = s.replace('\n', ';');
172         s = s.replace('\r', ';');
173         return s;
174     }
175 
176 
177     /**
178      * Returns configuration of the transaction served by this factory.
179      */
180     protected Hl7v2TransactionConfiguration getConfig() {
181         return config;
182     }
183 
184     /**
185      * Retrieves the given exception type from the exception.
186      * <p/>
187      * Is used to get the caused exception that typically have been wrapped in some sort
188      * of Camel wrapper exception
189      * <p/>
190      * The strategy is to look in the exception hierarchy to find the first given cause that matches the type.
191      * Will start from the bottom (the real cause) and walk upwards.
192      *
193      * @param type      the exception type wanted to retrieve
194      * @param exception the caused exception
195      * @return the exception found (or <tt>null</tt> if not found in the exception hierarchy)
196      */
197     private static <T extends Throwable> Optional<T> getException(Class<T> type, Throwable exception) {
198         if (exception == null) {
199             return Optional.empty();
200         }
201 
202         //check the suppressed exception first
203         Optional<T> result = identifyException(Stream.of(exception.getSuppressed()), type);
204         if (result.isPresent()) return result;
205         return createExceptionStream(exception)
206                 .filter(type::isInstance)
207                 .map(type::cast)
208                 .findFirst();
209     }
210 
211     private static <T extends Throwable> Optional<T> identifyException(Stream<Throwable> stream, Class<T> type) {
212         return stream
213                 .filter(type::isInstance)
214                 .map(type::cast)
215                 .findFirst();
216     }
217 
218     private static Stream<Throwable> createExceptionStream(Throwable exception) {
219         List<Throwable> throwables = new ArrayList<>();
220         Throwable current = exception;
221         while (current != null) {
222             throwables.add(current);
223             current = current.getCause();
224         }
225         Collections.reverse(throwables);
226         return throwables.stream();
227     }
228 
229 }