Repository: camel Updated Branches: refs/heads/camel-2.14.x f6dd81278 -> d76f78c59 refs/heads/master 4a5666d46 -> 4ba970668
CAMEL-8079: Provide possibility to delegate charset evaluation of a HL7 message to HL7DataFormat Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/4ba97066 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/4ba97066 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/4ba97066 Branch: refs/heads/master Commit: 4ba97066833b412877597e32cc32802dd2b18760 Parents: 4a5666d Author: Claus Ibsen <davscl...@apache.org> Authored: Wed Dec 3 20:10:56 2014 +0100 Committer: Claus Ibsen <davscl...@apache.org> Committed: Wed Dec 3 20:10:56 2014 +0100 ---------------------------------------------------------------------- .../org/apache/camel/component/hl7/HL7.java | 29 ++- .../apache/camel/component/hl7/HL7Charset.java | 123 +++++++++++ .../camel/component/hl7/HL7Constants.java | 1 + .../camel/component/hl7/HL7DataFormat.java | 47 ++-- .../camel/component/hl7/HL7MLLPCodec.java | 7 + .../camel/component/hl7/HL7MLLPConfig.java | 10 + .../camel/component/hl7/HL7MLLPDecoder.java | 28 ++- .../camel/component/hl7/AckExpressionTest.java | 7 +- .../component/hl7/HL7ByteArrayRouteTest.java | 219 +++++++++++++++++++ .../camel/component/hl7/HL7DataFormatTest.java | 93 +++++++- 10 files changed, 526 insertions(+), 38 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7.java index c1a6ae2..d9e8fdf 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7.java @@ -37,16 +37,23 @@ public final class HL7 { return new ValueBuilder(new TerserExpression(expression)); } - public static Expression ack() { - return new AckExpression(); + public static ValueBuilder ack() { + return new ValueBuilder(new AckExpression()); } - public static Expression ack(AckCode code) { - return new AckExpression(code); + /** + * @deprecated Use {@link #ack(ca.uhn.hl7v2.AcknowledgmentCode)} + */ + public static ValueBuilder ack(AckCode code) { + return new ValueBuilder(new AckExpression(code)); + } + + public static ValueBuilder ack(AcknowledgmentCode code) { + return new ValueBuilder(new AckExpression(code)); } - public static Expression convertLFToCR() { - return new ExpressionAdapter() { + public static ValueBuilder convertLFToCR() { + return new ValueBuilder(new ExpressionAdapter() { @Override public Object evaluate(Exchange exchange) { @@ -54,14 +61,14 @@ public final class HL7 { return s != null ? s.replace('\n', '\r') : null; } - }; + }); } /** * @deprecated Use {@link #ack(ca.uhn.hl7v2.AcknowledgmentCode, String, ErrorCode)} */ @Deprecated - public static Expression ack(AckCode code, String errorMessage, int errorCode) { + public static ValueBuilder ack(AckCode code, String errorMessage, int errorCode) { return ack(code.toAcknowledgmentCode(), errorMessage, ErrorCode.errorCodeFor(errorCode)); } @@ -69,12 +76,12 @@ public final class HL7 { * @deprecated Use {@link #ack(ca.uhn.hl7v2.AcknowledgmentCode, String, ErrorCode)} */ @Deprecated - public static Expression ack(AckCode code, String errorMessage, ErrorCode errorCode) { + public static ValueBuilder ack(AckCode code, String errorMessage, ErrorCode errorCode) { return ack(code.toAcknowledgmentCode(), errorMessage, errorCode); } - public static Expression ack(AcknowledgmentCode code, String errorMessage, ErrorCode errorCode) { - return new AckExpression(code, errorMessage, errorCode); + public static ValueBuilder ack(AcknowledgmentCode code, String errorMessage, ErrorCode errorCode) { + return new ValueBuilder(new AckExpression(code, errorMessage, errorCode)); } public static Predicate messageConforms() { http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Charset.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Charset.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Charset.java new file mode 100644 index 0000000..0e419ed --- /dev/null +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Charset.java @@ -0,0 +1,123 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.hl7; + +import java.io.UnsupportedEncodingException; + +import ca.uhn.hl7v2.HL7Exception; +import ca.uhn.hl7v2.model.Message; +import ca.uhn.hl7v2.model.Segment; +import ca.uhn.hl7v2.preparser.PreParser; +import org.apache.camel.Exchange; +import org.apache.camel.util.IOHelper; + +/** + * This enumerates the defined charsets for HL7 as defined in Table 0211, + * mapping them to the Java charset names and back + */ +public enum HL7Charset { + + ISO_8859_1("8859/1", "ISO-8859-1"), + ISO_8859_2("8859/2", "ISO-8859-2"), + ISO_8859_3("8859/3", "ISO-8859-3"), + ISO_8859_4("8859/4", "ISO-8859-4"), + ISO_8859_5("8859/5", "ISO-8859-5"), + ISO_8859_6("8859/1", "ISO-8859-6"), + ISO_8859_7("8859/1", "ISO-8859-7"), + ISO_8859_8("8859/1", "ISO-8859-8"), + ISO_8859_9("8859/1", "ISO-8859-9"), + ASCII("ASCII", "US-ASCII"), + BIG_5("BIG-5", "Big5"), + CNS("CNS 11643-1992", "ISO-2022-CN"), + GB_1830_2000("GB 18030-2000", ""), + ISO_IR14("ISO IR14", "ISO-2022-JP"), + ISO_IR159("ISO IR159", "EUC-JP"), + ISO_IR87("ISO IR87", "EUC-JP"), + KS_X_1001("KS X 1001", "EUC-KR"), + UNICODE("UNICODE", "UTF-8"), + UTF_16("UNICODE UTF-16", "UTF-16"), + UTF_32("UNICODE UTF-32", "UTF-32"), + UTF_8("UNICODE UTF-8", "UTF-8"); + + private String hl7CharsetName; + private String javaCharsetName; + + HL7Charset(String hl7CharsetName, String javaCharsetName) { + this.hl7CharsetName = hl7CharsetName; + this.javaCharsetName = javaCharsetName; + } + + public String getHL7CharsetName() { + return hl7CharsetName; + } + + public String getJavaCharsetName() { + return javaCharsetName; + } + + /** + * Returns the HL7Charset that matches the parameter + * + * @param s charset string + * @return HL7Charset enum + */ + public static HL7Charset getHL7Charset(String s) { + if (s != null && s.length() > 0) { + for (HL7Charset charset : HL7Charset.values()) { + if (charset.hl7CharsetName.equals(s) || charset.javaCharsetName.equals(s)) { + return charset; + } + } + } + return null; + } + + /** + * Returns the charset to be used for marshalling HL7 messages. If MSH-18 is empty, + * the charset configured in Camel's charset properties/headers is returned. + * + * @param message HL7 message + * @param exchange Exchange + * @return Java charset name + */ + public static String getCharsetName(Message message, Exchange exchange) throws HL7Exception { + String defaultCharsetName = IOHelper.getCharsetName(exchange); + String msh18 = ((Segment)message.get("MSH")).getField(18, 0).toString(); + return getCharsetName(msh18, defaultCharsetName); + } + + /** + * Returns the charset to be used for unmarshalling HL7 messages. If MSH-18 is empty, + * the temporary charset name is returned. + * + * @param bytes HL7 message as byte array + * @param guessedCharsetName the temporary charset guessed to be able to read MSH-18 + * @return Java charset name + * + * @see org.apache.camel.component.hl7.HL7DataFormat#guessCharsetName(byte[], org.apache.camel.Exchange) + */ + public static String getCharsetName(byte[] bytes, String guessedCharsetName) throws UnsupportedEncodingException, HL7Exception { + String tmp = new String(bytes, guessedCharsetName); + String msh18 = PreParser.getFields(tmp, "MSH-18")[0]; + return getCharsetName(msh18, guessedCharsetName); + } + + private static String getCharsetName(String msh18, String defaultCharsetName) { + HL7Charset charset = HL7Charset.getHL7Charset(msh18); + return charset != null ? charset.getJavaCharsetName() : defaultCharsetName; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Constants.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Constants.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Constants.java index 50b103d..05ca784 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Constants.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7Constants.java @@ -33,6 +33,7 @@ public final class HL7Constants { public static final String HL7_PROCESSING_ID = "CamelHL7ProcessingId"; public static final String HL7_VERSION_ID = "CamelHL7VersionId"; public static final String HL7_CONTEXT = "CamelHL7Context"; + public static final String HL7_CHARSET = "CamelHL7Charset"; private HL7Constants() { } http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7DataFormat.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7DataFormat.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7DataFormat.java index 7acbee8..6f2abf8 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7DataFormat.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7DataFormat.java @@ -33,18 +33,7 @@ import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.ExchangeHelper; import org.apache.camel.util.IOHelper; -import static org.apache.camel.component.hl7.HL7Constants.HL7_CONTEXT; -import static org.apache.camel.component.hl7.HL7Constants.HL7_MESSAGE_CONTROL; -import static org.apache.camel.component.hl7.HL7Constants.HL7_MESSAGE_TYPE; -import static org.apache.camel.component.hl7.HL7Constants.HL7_PROCESSING_ID; -import static org.apache.camel.component.hl7.HL7Constants.HL7_RECEIVING_APPLICATION; -import static org.apache.camel.component.hl7.HL7Constants.HL7_RECEIVING_FACILITY; -import static org.apache.camel.component.hl7.HL7Constants.HL7_SECURITY; -import static org.apache.camel.component.hl7.HL7Constants.HL7_SENDING_APPLICATION; -import static org.apache.camel.component.hl7.HL7Constants.HL7_SENDING_FACILITY; -import static org.apache.camel.component.hl7.HL7Constants.HL7_TIMESTAMP; -import static org.apache.camel.component.hl7.HL7Constants.HL7_TRIGGER_EVENT; -import static org.apache.camel.component.hl7.HL7Constants.HL7_VERSION_ID; +import static org.apache.camel.component.hl7.HL7Constants.*; /** * HL7 DataFormat (supports v2.x of the HL7 protocol). @@ -74,6 +63,7 @@ import static org.apache.camel.component.hl7.HL7Constants.HL7_VERSION_ID; * <li>CamelHL7MessageControl = MSH-10</li> * <li>CamelHL7ProcessingId = MSH-11</li> * <li>CamelHL7VersionId = MSH-12</li> + * <li>CamelHL7Charset = MSH-18</li> * </ul> * All headers are String types. * <p/> @@ -100,18 +90,21 @@ public class HL7DataFormat extends ServiceSupport implements DataFormat { HEADER_MAP.put(HL7_MESSAGE_CONTROL, "MSH-10"); HEADER_MAP.put(HL7_PROCESSING_ID, "MSH-11"); HEADER_MAP.put(HL7_VERSION_ID, "MSH-12"); + HEADER_MAP.put(HL7_CHARSET, "MSH-18"); } public void marshal(Exchange exchange, Object body, OutputStream outputStream) throws Exception { Message message = ExchangeHelper.convertToMandatoryType(exchange, Message.class, body); + String charsetName = HL7Charset.getCharsetName(message, exchange); String encoded = HL7Converter.encode(message, parser); - String charsetName = IOHelper.getCharsetName(exchange); outputStream.write(encoded.getBytes(charsetName)); } public Object unmarshal(Exchange exchange, InputStream inputStream) throws Exception { - String body = ExchangeHelper.convertToMandatoryType(exchange, String.class, inputStream); - Message message = HL7Converter.parse(body, parser); + byte[] body = ExchangeHelper.convertToMandatoryType(exchange, byte[].class, inputStream); + String charsetName = HL7Charset.getCharsetName(body, guessCharsetName(body, exchange)); + String bodyAsString = new String(body, charsetName); + Message message = HL7Converter.parse(bodyAsString, parser); // add MSH fields as message out headers Terser terser = new Terser(message); @@ -119,6 +112,7 @@ public class HL7DataFormat extends ServiceSupport implements DataFormat { exchange.getOut().setHeader(entry.getKey(), terser.get(entry.getValue())); } exchange.getOut().setHeader(HL7_CONTEXT, hapiContext); + exchange.getOut().setHeader(Exchange.CHARSET_NAME, charsetName); return message; } @@ -147,6 +141,7 @@ public class HL7DataFormat extends ServiceSupport implements DataFormat { } + @Override protected void doStart() throws Exception { if (hapiContext == null) { @@ -164,5 +159,27 @@ public class HL7DataFormat extends ServiceSupport implements DataFormat { protected void doStop() throws Exception { // noop } + + + /** + * In HL7 the charset of the message can be set in MSH-18, + * but you need to decode the input stream in order to be able to read MSH-18. + * This works well for differentiating e.g. between ASCII, UTF-8 and ISI-8859 charsets, + * but not for multi-byte charsets like UTF-16, Big5 etc. + * + * This method is called to "guess" the initial encoding, and subclasses can overwrite it + * using 3rd party libraries like ICU4J that provide a CharsetDetector. + * + * The implementation in this class just assumes the charset defined in the exchange property or header by + * calling {@link org.apache.camel.util.IOHelper#getCharsetName(org.apache.camel.Exchange)}. + * + * @param b byte array + * @param exchange + * @return charset name + */ + protected String guessCharsetName(byte[] b, Exchange exchange) { + return IOHelper.getCharsetName(exchange); + } + } http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java index 5fff6b6..8472d03 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java @@ -109,4 +109,11 @@ public class HL7MLLPCodec implements ProtocolCodecFactory { config.setValidate(validate); } + public boolean isProduceString() { + return config.isProduceString(); + } + + public void setProduceString(boolean apply) { + config.setProduceString(apply); + } } http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java index 61e4e45..4ec78d2 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java @@ -39,6 +39,8 @@ class HL7MLLPConfig { private Parser parser = hapiContext.getGenericParser(); + private boolean produceString = true; + public Charset getCharset() { return charset; } @@ -103,4 +105,12 @@ class HL7MLLPConfig { public void setValidate(boolean validate) { parser.getParserConfiguration().setValidating(validate); } + + public boolean isProduceString() { + return produceString; + } + + public void setProduceString(boolean produceString) { + this.produceString = produceString; + } } http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java index 3c616e1..4a54d47 100644 --- a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java +++ b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java @@ -72,7 +72,9 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder { // The bytes between in.position() and in.limit() // now contain a full MLLP message including the // start and end bytes. - out.write(parseMessage(in.slice(), charsetDecoder(session))); + out.write(config.isProduceString() + ? parseMessageToString(in.slice(), charsetDecoder(session)) + : parseMessageToByteArray(in.slice())); } catch (CharacterCodingException cce) { throw new IllegalArgumentException("Exception while finalizing the message", cce); } finally { @@ -99,9 +101,27 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder { // Make a defensive byte copy (the buffer will be reused) // and omit the start and the two end bytes of the MLLP message - // TODO: I wonder if it would make sense to return the plain byte array and let some subsequent - // processor do the conversion - private Object parseMessage(IoBuffer slice, CharsetDecoder decoder) throws CharacterCodingException { + // returning a byte array + private Object parseMessageToByteArray(IoBuffer slice) throws CharacterCodingException { + byte[] dst = new byte[slice.limit() - 3]; + slice.skip(1); // skip start byte + slice.get(dst, 0, dst.length); + + // Only do this if conversion is enabled + if (config.isConvertLFtoCR()) { + for (int i = 0; i < dst.length; i++) { + if (dst[i] == (byte)'\n') { + dst[i] = (byte)'\r'; + } + } + } + return dst; + } + + // Make a defensive byte copy (the buffer will be reused) + // and omit the start and the two end bytes of the MLLP message + // returning a String + private Object parseMessageToString(IoBuffer slice, CharsetDecoder decoder) throws CharacterCodingException { slice.skip(1); // skip start byte String message = slice.getString(slice.limit() - 3, decoder); http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/AckExpressionTest.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/AckExpressionTest.java b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/AckExpressionTest.java index e9be437..fa60d6d 100644 --- a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/AckExpressionTest.java +++ b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/AckExpressionTest.java @@ -15,6 +15,7 @@ * limitations under the License. */ package org.apache.camel.component.hl7; +import ca.uhn.hl7v2.AcknowledgmentCode; import ca.uhn.hl7v2.ErrorCode; import ca.uhn.hl7v2.HL7Exception; import ca.uhn.hl7v2.model.v24.message.ACK; @@ -91,13 +92,13 @@ public class AckExpressionTest extends CamelTestSupport { return new RouteBuilder() { public void configure() throws Exception { from("direct:test1").transform(ack()); - from("direct:test2").transform(ack(AckCode.CA)); + from("direct:test2").transform(ack(AcknowledgmentCode.CA)); from("direct:test3").onException(HL7Exception.class).handled(true).transform(ack()).end() .transform(terser("/.BLORG")); from("direct:test4").onException(HL7Exception.class).handled(true) - .transform(ack(AckCode.AR, "Problem!", ErrorCode.APPLICATION_INTERNAL_ERROR)).end() + .transform(ack(AcknowledgmentCode.AR, "Problem!", ErrorCode.APPLICATION_INTERNAL_ERROR)).end() .transform(terser("/.BLORG")); - from("direct:test5").transform(ack(AckCode.AR, "Problem!", ErrorCode.DATA_TYPE_ERROR)); + from("direct:test5").transform(ack(AcknowledgmentCode.AR, "Problem!", ErrorCode.DATA_TYPE_ERROR)); } }; } http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7ByteArrayRouteTest.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7ByteArrayRouteTest.java b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7ByteArrayRouteTest.java new file mode 100644 index 0000000..38da5df --- /dev/null +++ b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7ByteArrayRouteTest.java @@ -0,0 +1,219 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.hl7; + +import ca.uhn.hl7v2.model.Message; +import ca.uhn.hl7v2.model.v24.message.ADR_A19; +import ca.uhn.hl7v2.model.v24.message.ADT_A01; +import ca.uhn.hl7v2.model.v24.message.QRY_A19; +import ca.uhn.hl7v2.model.v24.segment.MSA; +import ca.uhn.hl7v2.model.v24.segment.MSH; +import ca.uhn.hl7v2.model.v24.segment.PID; +import ca.uhn.hl7v2.model.v24.segment.QRD; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.impl.JndiRegistry; +import org.apache.camel.spi.DataFormat; +import org.junit.Test; + +/** + * Unit test for HL7 routing where the mina endpoint passes on a byte array instead of a string + * and leaves charset interpretation to the dataformat. + */ +public class HL7ByteArrayRouteTest extends HL7TestSupport { + + protected JndiRegistry createRegistry() throws Exception { + JndiRegistry jndi = super.createRegistry(); + + HL7MLLPCodec codec = new HL7MLLPCodec(); + codec.setProduceString(false); + + jndi.bind("hl7codec", codec); + + MyHL7BusinessLogic logic = new MyHL7BusinessLogic(); + jndi.bind("hl7service", logic); + + return jndi; + } + + @Test + public void testSendA19() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:a19"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(Message.class); + + String line1 = "MSH|^~\\&|MYSENDER|MYSENDERAPP|MYCLIENT|MYCLIENTAPP|200612211200||QRY^A19|1234|P|2.4"; + String line2 = "QRD|200612211200|R|I|GetPatient|||1^RD|0101701234|DEM||"; + + StringBuilder in = new StringBuilder(); + in.append(line1); + in.append("\r"); + in.append(line2); + + String out = template.requestBody("mina2:tcp://127.0.0.1:" + getPort() + "?sync=true&codec=#hl7codec", in.toString(), String.class); + + String[] lines = out.split("\r"); + assertEquals("MSH|^~\\&|MYSENDER||||200701011539||ADR^A19||||123|||||UNICODE UTF-8", lines[0]); + assertEquals("MSA|AA|123", lines[1]); + + assertMockEndpointsSatisfied(); + } + + @Test + public void testSendA01() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:a01"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(Message.class); + + String line1 = "MSH|^~\\&|MYSENDER|MYSENDERAPP|MYCLIENT|MYCLIENTAPP|200612211200||ADT^A01|123|P|2.4||||||UNICODE UTF-8"; + String line2 = "PID|||123456||Döe^John"; + + StringBuilder in = new StringBuilder(); + in.append(line1); + in.append("\r"); + in.append(line2); + + String out = template.requestBody("mina2:tcp://127.0.0.1:" + getPort() + "?sync=true&codec=#hl7codec", in.toString(), String.class); + String[] lines = out.split("\r"); + assertEquals("MSH|^~\\&|MYSENDER||||200701011539||ADT^A01||||123|||||UNICODE UTF-8", lines[0]); + assertEquals("PID|||123||Döe^John", lines[1]); + + assertMockEndpointsSatisfied(); + } + + @Test + public void testSendUnknown() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:unknown"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(Message.class); + + String line1 = "MSH|^~\\&|MYSENDER|MYSENDERAPP|MYCLIENT|MYCLIENTAPP|200612211200||ADT^A02|1234|P|2.4"; + String line2 = "PID|||123456||Döe^John"; + + StringBuilder in = new StringBuilder(); + in.append(line1); + in.append("\r"); + in.append(line2); + + template.requestBody("mina2:tcp://127.0.0.1:" + getPort() + "?sync=true&codec=#hl7codec", in.toString()); + + assertMockEndpointsSatisfied(); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + public void configure() throws Exception { + // START SNIPPET: e1 + DataFormat hl7 = new HL7DataFormat(); + // we setup or HL7 listener on port 8888 (using the hl7codec) and in sync mode so we can return a response + from("mina2:tcp://127.0.0.1:" + getPort() + "?sync=true&codec=#hl7codec") + // we use the HL7 data format to unmarshal from HL7 stream to the HAPI Message model + // this ensures that the camel message has been enriched with hl7 specific headers to + // make the routing much easier (see below) + .unmarshal(hl7) + // using choice as the content base router + .choice() + // where we choose that A19 queries invoke the handleA19 method on our hl7service bean + .when(header("CamelHL7TriggerEvent").isEqualTo("A19")) + .beanRef("hl7service", "handleA19") + .to("mock:a19") + // and A01 should invoke the handleA01 method on our hl7service bean + .when(header("CamelHL7TriggerEvent").isEqualTo("A01")).to("mock:a01") + .beanRef("hl7service", "handleA01") + .to("mock:a19") + // other types should go to mock:unknown + .otherwise() + .to("mock:unknown") + // end choice block + .end() + // marshal response back + .marshal(hl7); + // END SNIPPET: e1 + } + }; + } + + public class MyHL7BusinessLogic { + + // This is a plain POJO that has NO imports whatsoever on Apache Camel. + // its a plain POJO only importing the HAPI library so we can much easier work with the HL7 format. + + public Message handleA19(Message msg) throws Exception { + // here you can have your business logic for A19 messages + assertTrue(msg instanceof QRY_A19); + // just return the same dummy response + return createADR19Message(); + } + + public Message handleA01(Message msg) throws Exception { + // here you can have your business logic for A01 messages + assertTrue(msg instanceof ADT_A01); + // just return the same dummy response + return createADT01Message(((ADT_A01)msg).getMSH().getMessageControlID().getValue()); + } + } + + private static Message createADR19Message() throws Exception { + ADR_A19 adr = new ADR_A19(); + + // Populate the MSH Segment + MSH mshSegment = adr.getMSH(); + mshSegment.getFieldSeparator().setValue("|"); + mshSegment.getEncodingCharacters().setValue("^~\\&"); + mshSegment.getDateTimeOfMessage().getTimeOfAnEvent().setValue("200701011539"); + mshSegment.getSendingApplication().getNamespaceID().setValue("MYSENDER"); + mshSegment.getSequenceNumber().setValue("123"); + mshSegment.getMessageType().getMessageType().setValue("ADR"); + mshSegment.getMessageType().getTriggerEvent().setValue("A19"); + mshSegment.getCharacterSet(0).setValue("UNICODE UTF-8"); + + // Populate the PID Segment + MSA msa = adr.getMSA(); + msa.getAcknowledgementCode().setValue("AA"); + msa.getMessageControlID().setValue("123"); + + QRD qrd = adr.getQRD(); + qrd.getQueryDateTime().getTimeOfAnEvent().setValue("20080805120000"); + + return adr.getMessage(); + } + + private static Message createADT01Message(String msgId) throws Exception { + ADT_A01 adt = new ADT_A01(); + + // Populate the MSH Segment + MSH mshSegment = adt.getMSH(); + mshSegment.getFieldSeparator().setValue("|"); + mshSegment.getEncodingCharacters().setValue("^~\\&"); + mshSegment.getDateTimeOfMessage().getTimeOfAnEvent().setValue("200701011539"); + mshSegment.getSendingApplication().getNamespaceID().setValue("MYSENDER"); + mshSegment.getSequenceNumber().setValue("123"); + mshSegment.getMessageType().getMessageType().setValue("ADT"); + mshSegment.getMessageType().getTriggerEvent().setValue("A01"); + mshSegment.getCharacterSet(0).setValue("UNICODE UTF-8"); + + // Populate the PID Segment + PID pid = adt.getPID(); + pid.getPatientName(0).getFamilyName().getSurname().setValue("Döe"); + pid.getPatientName(0).getGivenName().setValue("John"); + pid.getPatientIdentifierList(0).getID().setValue(msgId); + + return adt; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/4ba97066/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7DataFormatTest.java ---------------------------------------------------------------------- diff --git a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7DataFormatTest.java b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7DataFormatTest.java index 8d1444b..4334f9b 100644 --- a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7DataFormatTest.java +++ b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7DataFormatTest.java @@ -16,6 +16,9 @@ */ package org.apache.camel.component.hl7; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; + import ca.uhn.hl7v2.model.Message; import ca.uhn.hl7v2.model.v24.message.ADR_A19; import ca.uhn.hl7v2.model.v24.segment.MSA; @@ -34,7 +37,13 @@ public class HL7DataFormatTest extends CamelTestSupport { private static final String NONE_ISO_8859_1 = "\u221a\u00c4\u221a\u00e0\u221a\u00e5\u221a\u00ed\u221a\u00f4\u2248\u00ea"; - private HL7DataFormat hl7; + private HL7DataFormat hl7 = new HL7DataFormat(); + private HL7DataFormat hl7big5 = new HL7DataFormat() { + @Override + protected String guessCharsetName(byte[] b, Exchange exchange) { + return "Big5"; + } + }; @Test public void testMarshal() throws Exception { @@ -62,6 +71,22 @@ public class HL7DataFormatTest extends CamelTestSupport { template.sendBodyAndProperty("direct:marshal", message, Exchange.CHARSET_NAME, "ISO-8859-1"); assertMockEndpointsSatisfied(); } + + @Test + public void testMarshalUTF16InMessage() throws Exception { + String charsetName = "UTF-16"; + MockEndpoint mock = getMockEndpoint("mock:marshal"); + mock.expectedMessageCount(1); + + Message message = createHL7WithCharsetAsMessage(HL7Charset.getHL7Charset(charsetName)); + template.sendBodyAndProperty("direct:marshal", message, Exchange.CHARSET_NAME, charsetName); + assertMockEndpointsSatisfied(); + + byte[] body = (byte[])mock.getExchanges().get(0).getIn().getBody(); + String msg = new String(body, Charset.forName(charsetName)); + assertTrue(msg.contains("MSA|AA|123")); + assertTrue(msg.contains("QRD|20080805120000")); + } @Test public void testMarshalUTF8() throws Exception { @@ -94,6 +119,9 @@ public class HL7DataFormatTest extends CamelTestSupport { mock.expectedHeaderReceived(HL7Constants.HL7_PROCESSING_ID, "P"); mock.expectedHeaderReceived(HL7Constants.HL7_VERSION_ID, "2.4"); mock.expectedHeaderReceived(HL7Constants.HL7_CONTEXT, hl7.getHapiContext()); + mock.expectedHeaderReceived(HL7Constants.HL7_CHARSET, null); + + mock.expectedHeaderReceived(Exchange.CHARSET_NAME, "UTF-8"); String body = createHL7AsString(); template.sendBody("direct:unmarshal", body); @@ -106,19 +134,68 @@ public class HL7DataFormatTest extends CamelTestSupport { assertEquals("0101701234", qrd.getWhoSubjectFilter(0).getIDNumber().getValue()); } + @Test + public void testUnmarshalWithExplicitUTF16Charset() throws Exception { + String charset = "UTF-16"; + MockEndpoint mock = getMockEndpoint("mock:unmarshal"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(Message.class); + mock.expectedHeaderReceived(HL7Constants.HL7_CHARSET, HL7Charset.getHL7Charset(charset).getHL7CharsetName()); + mock.expectedHeaderReceived(Exchange.CHARSET_NAME, charset); + + // Message with explicit encoding in MSH-18 + byte[] body = createHL7WithCharsetAsString(HL7Charset.UTF_16).getBytes(Charset.forName(charset)); + template.sendBodyAndHeader("direct:unmarshal", new ByteArrayInputStream(body), Exchange.CHARSET_NAME, charset); + + assertMockEndpointsSatisfied(); + + Message msg = mock.getExchanges().get(0).getIn().getBody(Message.class); + assertEquals("2.4", msg.getVersion()); + QRD qrd = (QRD) msg.get("QRD"); + assertEquals("0101701234", qrd.getWhoSubjectFilter(0).getIDNumber().getValue()); + } + + @Test + public void testUnmarshalWithImplicitBig5Charset() throws Exception { + String charset = "Big5"; + MockEndpoint mock = getMockEndpoint("mock:unmarshalBig5"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(Message.class); + mock.expectedHeaderReceived(HL7Constants.HL7_CHARSET, null); + mock.expectedHeaderReceived(Exchange.CHARSET_NAME, charset); + + // Message without explicit encoding in MSH-18, but the unmarshaller "guesses" + // this time that it is Big5 + byte[] body = createHL7AsString().getBytes(Charset.forName(charset)); + template.sendBody("direct:unmarshalBig5", new ByteArrayInputStream(body)); + + assertMockEndpointsSatisfied(); + + Message msg = mock.getExchanges().get(0).getIn().getBody(Message.class); + assertEquals("2.4", msg.getVersion()); + QRD qrd = (QRD) msg.get("QRD"); + assertEquals("0101701234", qrd.getWhoSubjectFilter(0).getIDNumber().getValue()); + } + protected RouteBuilder createRouteBuilder() throws Exception { return new RouteBuilder() { public void configure() throws Exception { - hl7 = new HL7DataFormat(); + from("direct:marshal").marshal().hl7().to("mock:marshal"); from("direct:unmarshal").unmarshal(hl7).to("mock:unmarshal"); + from("direct:unmarshalBig5").unmarshal(hl7big5).to("mock:unmarshalBig5"); } }; } private static String createHL7AsString() { - String line1 = "MSH|^~\\&|MYSENDER|MYSENDERAPP|MYCLIENT|MYCLIENTAPP|200612211200||QRY^A19|1234|P|2.4"; + return createHL7WithCharsetAsString(null); + } + + private static String createHL7WithCharsetAsString(HL7Charset charset) { + String hl7Charset = charset == null ? "" : charset.getHL7CharsetName(); + String line1 = String.format("MSH|^~\\&|MYSENDER|MYSENDERAPP|MYCLIENT|MYCLIENTAPP|200612211200||QRY^A19|1234|P|2.4||||||%s", hl7Charset); String line2 = "QRD|200612211200|R|I|GetPatient|||1^RD|0101701234|DEM||"; StringBuilder body = new StringBuilder(); @@ -128,7 +205,7 @@ public class HL7DataFormatTest extends CamelTestSupport { return body.toString(); } - private static Message createHL7AsMessage() throws Exception { + private static ADR_A19 createHL7AsMessage() throws Exception { ADR_A19 adr = new ADR_A19(); // Populate the MSH Segment @@ -150,7 +227,13 @@ public class HL7DataFormatTest extends CamelTestSupport { QRD qrd = adr.getQRD(); qrd.getQueryDateTime().getTimeOfAnEvent().setValue("20080805120000"); - return adr.getMessage(); + return adr; + } + + private static ADR_A19 createHL7WithCharsetAsMessage(HL7Charset charset) throws Exception { + ADR_A19 adr = createHL7AsMessage(); + adr.getMSH().getCharacterSet(0).setValue(charset.getHL7CharsetName()); + return adr; } }