001 /**
002 The contents of this file are subject to the Mozilla Public License Version 1.1
003 (the "License"); you may not use this file except in compliance with the License.
004 You may obtain a copy of the License at http://www.mozilla.org/MPL/
005 Software distributed under the License is distributed on an "AS IS" basis,
006 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007 specific language governing rights and limitations under the License.
008
009 The Initial Developer of the Original Code is University Health Network. Copyright (C)
010 2001. All Rights Reserved.
011
012 Contributor(s): Jens Kristian Villadsen from Cetrea A/S
013
014 Alternatively, the contents of this file may be used under the terms of the
015 GNU General Public License (the "GPL"), in which case the provisions of the GPL are
016 applicable instead of those above. If you wish to allow use of your version of this
017 file only under the terms of the GPL and not to allow others to use your version
018 of this file under the MPL, indicate your decision by deleting the provisions above
019 and replace them with the notice and other provisions required by the GPL License.
020 If you do not delete the provisions above, a recipient may use your version of
021 this file under either the MPL or the GPL.
022
023 */
024
025 package ca.uhn.hl7v2.llp;
026
027 import static ca.uhn.hl7v2.llp.MinLLPReader.*;
028
029 import java.io.ByteArrayInputStream;
030 import java.io.ByteArrayOutputStream;
031 import java.io.IOException;
032 import java.io.InputStream;
033 import java.io.InputStreamReader;
034 import java.net.SocketException;
035 import java.net.SocketTimeoutException;
036 import java.nio.charset.Charset;
037
038 import org.slf4j.Logger;
039 import org.slf4j.LoggerFactory;
040
041 import ca.uhn.hl7v2.preparser.PreParser;
042
043 /**
044 * Charset-aware MLLP stream reader
045 *
046 * @see ExtendedMinLowerLayerProtocol
047 * @author Jens Kristian Villadsen from Cetrea A/S
048 */
049 public class ExtendedMinLLPReader implements HL7Reader
050 {
051
052 private static final Logger log = LoggerFactory.getLogger(ExtendedMinLLPReader.class);
053
054 private InputStream inputStream;
055 private Charset myLastCharset;
056 private InputStreamReader myReader;
057
058 /**
059 * Creates a MinLLPReader with no setup - setInputStream must be set later.
060 */
061 public ExtendedMinLLPReader()
062 {
063 super();
064 }
065
066 /**
067 * Creates a MinLLPReader which reads from the given InputStream. The stream is assumed to be an ASCII bit stream.
068 */
069 public ExtendedMinLLPReader(InputStream in) throws IOException
070 {
071 setInputStream(in);
072 }
073
074 /**
075 * Closes the underlying BufferedReader.
076 */
077 public synchronized void close() throws java.io.IOException
078 {
079 myReader.close();
080 }
081
082 private Charset getCharacterEncoding(InputStream in) throws IOException
083 {
084 ByteArrayOutputStream bos = new ByteArrayOutputStream();
085 int next = in.read();
086 while((next != -1 || bos.size() == 0) && next != END_MESSAGE && next != LAST_CHARACTER)
087 {
088 bos.write(next);
089 next = in.read();
090 }
091 bos.flush();
092
093 try
094 {
095 String firstLine;
096 if ((bos.toByteArray()[0] == -2 && bos.toByteArray()[1] == -1) ||
097 bos.toByteArray()[1] == -2 && bos.toByteArray()[0] == -1) {
098
099 // if the string is little endian, then we will be missing the second byte of the
100 // last character (a "\r"), so add it manually
101 if (bos.toByteArray()[1] == -2 && bos.toByteArray()[0] == -1) {
102 bos.write(0);
103 }
104
105 firstLine = bos.toString("UTF-16");
106 } else {
107 firstLine = bos.toString("US-ASCII");
108 }
109
110 String[] fields = PreParser.getFields(firstLine, "MSH-18(0)");
111 String charset = stripNonLowAscii(fields[0]);
112 Charset javaCs = CharSetUtil.convertHL7CharacterEncodingToCharSetvalue(charset);
113 log.debug("Detected MSH-18 value \"{}\" so using charset {}", charset, javaCs.displayName());
114 return javaCs;
115 }
116 catch(Exception e)
117 {
118 log.warn("Nonvalid charset - defaulting to US-ASCII", e);
119 }
120 finally
121 {
122 bos.close();
123 }
124 return Charset.forName("US-ASCII");
125 }
126
127 private String stripNonLowAscii(String theString) {
128 if (theString == null) return "";
129 StringBuilder b = new StringBuilder();
130
131 for (int i = 0; i < theString.length(); i++) {
132 char next = theString.charAt(i);
133 if (next > 0 && next < 127) {
134 b.append(next);
135 }
136 }
137
138 return b.toString();
139 }
140
141 /**
142 * @return the lastCharset
143 */
144 public Charset getLastCharset() {
145 return myLastCharset;
146 }
147
148 public synchronized String getMessage() throws LLPException, IOException
149 {
150 ByteArrayOutputStream baos = null;
151 baos = verifyAndCopyToOutputStream(this.inputStream);
152
153 if(baos == null)
154 return null;
155
156 byte[] byteArray = baos.toByteArray();
157 myLastCharset = getCharacterEncoding(new ByteArrayInputStream(byteArray));
158
159 myReader = new InputStreamReader(new ByteArrayInputStream(byteArray), myLastCharset);
160 baos.close();
161
162 StringBuffer s_buffer = new StringBuffer();
163
164 int c = myReader.read();
165 while(c != -1)
166 {
167 s_buffer.append((char) c);
168 c = myReader.read();
169 }
170 return s_buffer.toString();
171 }
172
173 /**
174 * Sets the InputStream from which to read messages. The InputStream must be set before any calls to <code>getMessage()</code>.
175 */
176 public synchronized void setInputStream(InputStream in) throws IOException
177 {
178 this.inputStream = in;
179 }
180
181 private ByteArrayOutputStream verifyAndCopyToOutputStream(InputStream stream) throws IOException, LLPException
182 {
183 ByteArrayOutputStream bos = new ByteArrayOutputStream();
184 boolean end_of_message = false;
185
186 int c = 0;
187 try
188 {
189 c = stream.read();
190 }
191 catch(SocketException e)
192 {
193 log.info("SocketException on read() attempt. Socket appears to have been closed: {}", e.getMessage());
194 throw e;
195 }
196 catch(SocketTimeoutException e)
197 {
198 log.debug("SocketTimeoutException on read() attempt.");
199 return null;
200 }
201 // trying to read when there is no data (stream may have been closed at other end)
202 if(c == -1)
203 {
204 log.info("End of input stream reached.");
205 throw new SocketException("End of input stream reached");
206 }
207 LowerLayerProtocol.logCharacterReceived(c);
208
209 if(c != START_MESSAGE)
210 {
211 throw new LLPException("Message violates the " + "minimal lower layer protocol: no start of message indicator " + "received. Received: " + c);
212 }
213
214 while(!end_of_message)
215 {
216 c = stream.read();
217
218 if(c == -1)
219 {
220 throw new LLPException("Message violates the " + "minimal lower protocol: message terminated without " + "a terminating character.");
221 }
222 LowerLayerProtocol.logCharacterReceived(c);
223
224 if(c == END_MESSAGE)
225 {
226 // subsequent character should be a carriage return
227 c = stream.read();
228 if(c >= 0)
229 LowerLayerProtocol.logCharacterReceived(c);
230 if(c != LAST_CHARACTER)
231 {
232 throw new LLPException("Message " + "violates the minimal lower layer protocol: " + "message terminator not followed by a return " + "character.");
233 }
234 end_of_message = true;
235 }
236 else
237 {
238 // the character wasn't the end of message, append it to the message
239 bos.write(c);
240 }
241 }
242
243 bos.flush();
244 return bos;
245 }
246 }