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 }