Coverage Report - ca.uhn.hl7v2.hoh.encoder.AbstractHl7OverHttpDecoder
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractHl7OverHttpDecoder
76%
225/295
69%
89/128
5.12
 
 1  
 package ca.uhn.hl7v2.hoh.encoder;
 2  
 
 3  
 import java.io.ByteArrayOutputStream;
 4  
 import java.io.IOException;
 5  
 import java.io.InputStream;
 6  
 import java.net.SocketException;
 7  
 import java.net.SocketTimeoutException;
 8  
 import java.nio.charset.Charset;
 9  
 import java.nio.charset.UnsupportedCharsetException;
 10  
 import java.util.ArrayList;
 11  
 import java.util.LinkedHashMap;
 12  
 import java.util.List;
 13  
 import java.util.Map;
 14  
 import java.util.regex.Pattern;
 15  
 
 16  
 import ca.uhn.hl7v2.hoh.api.DecodeException;
 17  
 import ca.uhn.hl7v2.hoh.api.NonHl7ResponseException;
 18  
 import ca.uhn.hl7v2.hoh.sign.SignatureFailureException;
 19  
 import ca.uhn.hl7v2.hoh.sign.SignatureVerificationException;
 20  
 import ca.uhn.hl7v2.hoh.util.ByteUtils;
 21  
 import ca.uhn.hl7v2.hoh.util.GZipUtils;
 22  
 import ca.uhn.hl7v2.hoh.util.IOUtils;
 23  
 import ca.uhn.hl7v2.hoh.util.StringUtils;
 24  
 import ca.uhn.hl7v2.hoh.util.repackage.Base64;
 25  
 
 26  9500
 public abstract class AbstractHl7OverHttpDecoder extends AbstractHl7OverHttp {
 27  
 
 28  5
         private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
 29  
 
 30  
         /**
 31  
          * Default amount of time that the decoder will attempt to read before
 32  
          * timing out and throwing an IOException (30000ms)
 33  
          * 
 34  
          * @see #setReadTimeout(long)
 35  
          */
 36  
         public static final int DEFAULT_READ_TIMEOUT = 30 * 1000;
 37  
 
 38  5
         private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AbstractHl7OverHttpDecoder.class);
 39  
 
 40  
         private byte[] myBytes;
 41  
         private List<String> myConformanceProblems;
 42  510
         private int myContentLength = -1;
 43  
         private String myContentType;
 44  
         private boolean myGzipCoding;
 45  
         private long myLastStartedReading;
 46  510
         private long myReadTimeout = DEFAULT_READ_TIMEOUT;
 47  
         private String myResponseName;
 48  
         private Integer myResponseStatus;
 49  
         private TransferEncoding myTransferEncoding;
 50  
         private String mySignature;
 51  
         private EncodingStyle myEncodingStyle;
 52  
 
 53  
         private boolean myConnectionCloseHeaderIsPresent;
 54  
 
 55  
         private void addConformanceProblem(String theString) {
 56  0
                 ourLog.debug("Conformance problem detected: {}", theString);
 57  0
                 if (myConformanceProblems == null) {
 58  0
                         myConformanceProblems = new ArrayList<String>();
 59  
                 }
 60  0
                 myConformanceProblems.add(theString);
 61  0
         }
 62  
 
 63  
         protected abstract void authorize() throws AuthorizationFailureException;
 64  
 
 65  
         public void decode() throws DecodeException, SignatureVerificationException {
 66  0
                 ourLog.trace("Entering decode()");
 67  
                 
 68  0
                 verifyNotUsed();
 69  
 
 70  0
                 decodeHeaders();
 71  0
                 authorize();
 72  0
                 decodeBody();
 73  0
                 verifySignature();
 74  
 
 75  0
                 ourLog.trace("Exiting decode()");
 76  0
         }
 77  
 
 78  
         private void decodeBody() throws DecodeException {
 79  415
                 byte[] bytes = myBytes;
 80  
 
 81  415
                 if (myGzipCoding) {
 82  10
                         ourLog.debug("Decoding message contents using GZIP encoding style");
 83  
                         try {
 84  10
                                 bytes = GZipUtils.uncompress(bytes);
 85  0
                         } catch (IOException e) {
 86  0
                                 throw new DecodeException("Failed to uncompress GZip content", e);
 87  10
                         }
 88  
                 }
 89  
 
 90  415
                 Charset charset = getCharset();
 91  
                 
 92  415
                 ourLog.debug("Message is {} bytes with charset {}", bytes.length, charset.name());
 93  415
                 if (ourLog.isTraceEnabled()) {
 94  20
                         ourLog.trace("Raw message: {}", StringUtils.asciiEscape(bytes, charset));
 95  
                 }
 96  
                 
 97  415
                 String messageString = new String(bytes, charset);
 98  415
                 setMessage(messageString);
 99  415
         }
 100  
 
 101  
         private void decodeHeaders() throws DecodeException {
 102  
 
 103  415
                 ourLog.trace("Header map contains: {}", getHeaders());
 104  
 
 105  415
                 for (Map.Entry<String, String> nextEntry : getHeaders().entrySet()) {
 106  2285
                         String nextHeader = nextEntry.getKey().toLowerCase();
 107  2285
                         String nextValue = nextEntry.getValue();
 108  
 
 109  2285
                         ourLog.trace("Next header: {}={}", nextHeader, nextValue);
 110  
                         
 111  2285
                         if ("transfer-encoding".equals(nextHeader)) {
 112  30
                                 if ("chunked".equalsIgnoreCase(nextValue)) {
 113  30
                                         myTransferEncoding = TransferEncoding.CHUNKED;
 114  30
                                         ourLog.trace("Found chunked transfer encoding");
 115  
                                 } else {
 116  0
                                         throw new DecodeException("Unknown transfer encoding: " + nextValue);
 117  
                                 }
 118  2255
                         } else if ("connection".equals(nextHeader)) {
 119  95
                                 if ("close".equals(nextValue)) {
 120  30
                                         myConnectionCloseHeaderIsPresent = true;
 121  
                                 }
 122  2160
                         } else if ("content-length".equals(nextHeader)) {
 123  
                                 try {
 124  385
                                         myContentLength = Integer.parseInt(nextValue);
 125  385
                                         ourLog.trace("Found content length: {}", myContentLength);
 126  0
                                 } catch (NumberFormatException e) {
 127  0
                                         addConformanceProblem("Could not parse Content-Length header value: " + nextHeader);
 128  385
                                 }
 129  1775
                         } else if ("content-type".equals(nextHeader)) {
 130  415
                                 int colonIndex = nextValue.indexOf(';');
 131  415
                                 if (colonIndex == -1) {
 132  5
                                         myContentType = nextValue.trim();
 133  
                                 } else {
 134  410
                                         myContentType = nextValue.substring(0, colonIndex).trim();
 135  410
                                         String charsetDef = nextValue.substring(colonIndex + 1).trim();
 136  410
                                         if (charsetDef.startsWith("charset=")) {
 137  410
                                                 String charsetName = charsetDef.substring(8);
 138  
                                                 Charset charset;
 139  
                                                 try {
 140  410
                                                         charset = Charset.forName(charsetName);
 141  0
                                                 } catch (UnsupportedCharsetException e) {
 142  0
                                                         addConformanceProblem("Unsupported or invalid charset: " + charsetName);
 143  0
                                                         continue;
 144  410
                                                 }
 145  410
                                                 setCharset(charset);
 146  
                                         }
 147  
                                 }
 148  
 
 149  415
                                 myEncodingStyle = EncodingStyle.getEncodingStyleForContentType(myContentType);
 150  415
                                 ourLog.trace("Found content type {} with resolves to encoding style {}", myContentType, myEncodingStyle);
 151  
 
 152  415
                         } else if ("authorization".equals(nextHeader)) {
 153  250
                                 int spaceIndex = nextValue.indexOf(' ');
 154  250
                                 if (spaceIndex == -1) {
 155  0
                                         throw new DecodeException("Invalid authorization header. No authorization style detected");
 156  
                                 }
 157  250
                                 String type = nextValue.substring(0, spaceIndex);
 158  250
                                 if ("basic".equalsIgnoreCase(type)) {
 159  250
                                         String encodedCredentials = nextValue.substring(spaceIndex + 1);
 160  250
                                         byte[] decodedCredentials = Base64.decodeBase64(encodedCredentials);
 161  250
                                         String credentialsString = new String(decodedCredentials, getDefaultCharset());
 162  250
                                         int colonIndex = credentialsString.indexOf(':');
 163  250
                                         if (colonIndex == -1) {
 164  0
                                                 setUsername(credentialsString);
 165  
                                         } else {
 166  250
                                                 setUsername(credentialsString.substring(0, colonIndex));
 167  250
                                                 setPassword(credentialsString.substring(colonIndex + 1));
 168  
                                         }
 169  
                                         
 170  250
                                         ourLog.trace("Found authorization header with username: {}", getUsername());
 171  
                                         
 172  250
                                 } else {
 173  0
                                         addConformanceProblem("Invalid authorization type. Only basic authorization is supported.");
 174  
                                 }
 175  
                                 
 176  250
                         } else if ("content-encoding".equals(nextHeader)) {
 177  10
                                 if (StringUtils.isNotBlank(nextValue)) {
 178  10
                                         if ("gzip".equals(nextValue)) {
 179  10
                                                 myGzipCoding = true;
 180  
                                         } else {
 181  0
                                                 throw new DecodeException("Unknown Content-Encoding: " + nextValue);
 182  
                                         }
 183  
                                 }
 184  10
                                 ourLog.trace("Found content coding: {}", nextValue);
 185  1100
                         } else if (HTTP_HEADER_HL7_SIGNATURE_LC.equals(nextHeader)) {
 186  5
                                 ourLog.trace("Found signature: {}", nextValue);
 187  5
                                 mySignature = nextValue;
 188  
                         } else {
 189  1095
                                 ourLog.trace("Ignoring header {}={}", nextHeader, nextValue);
 190  
                         }
 191  
 
 192  2285
                 }
 193  
 
 194  415
                 ourLog.trace("Done processing headers");
 195  
 
 196  415
         }
 197  
 
 198  
         /**
 199  
          * Protected because this doesn't make sense for a sender
 200  
          */
 201  
         protected boolean isConnectionCloseHeaderPresent() {
 202  110
                 return myConnectionCloseHeaderIsPresent;
 203  
         }
 204  
 
 205  
         /**
 206  
          * Returns the {@link EncodingStyle} associated with the incoming message,
 207  
          * or <code>null</code>. This will be set automatically based on the value
 208  
          * of the <code>Content-Type</code> header, and will be set to
 209  
          * <code>null</code> if the content type is not provided, or if the content
 210  
          * type does not correspond to an HL7 type.
 211  
          * 
 212  
          * @see {@link EncodingStyle} for a list of appropriate content types
 213  
          */
 214  
         public EncodingStyle getEncodingStyle() {
 215  540
                 return myEncodingStyle;
 216  
         }
 217  
 
 218  
         private void doReadContentsFromInputStreamAndDecode(InputStream theInputStream) throws DecodeException, AuthorizationFailureException, IOException, SignatureVerificationException {
 219  415
                 decodeHeaders();
 220  415
                 authorize();
 221  415
                 if (myTransferEncoding == TransferEncoding.CHUNKED) {
 222  30
                         myBytes = readBytesChunked(theInputStream);
 223  
                 } else {
 224  385
                         myBytes = readBytesNonChunked(theInputStream);
 225  
                 }
 226  
 
 227  415
                 decodeBody();
 228  
                 
 229  415
                 if (getContentType() == null) {
 230  0
                         throw new DecodeException("Content-Type not specified");
 231  
                 }
 232  415
                 if (getEncodingStyle() == null) {
 233  0
                         throw new NonHl7ResponseException("Invalid Content-Type: " + getContentType(), getContentType(), getMessage());
 234  
                 }
 235  
                 
 236  415
                 verifySignature();
 237  415
         }
 238  
 
 239  
         private byte[] readBytesChunked(InputStream theInputStream) throws DecodeException, IOException {
 240  30
                 ourLog.debug("Decoding message bytes using CHUNKED encoding style");
 241  30
                 byte[] byteBuffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
 242  30
                 ByteArrayOutputStream bos = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE);
 243  
 
 244  
                 while (true) {
 245  
                         String nextSize;
 246  
                         try {
 247  70
                                 nextSize = readLine(theInputStream);
 248  0
                         } catch (IOException e) {
 249  0
                                 throw new DecodeException("Failed to decode CHUNKED encoding", e);
 250  70
                         }
 251  
 
 252  70
                         ourLog.trace("Going to interpret CHUNKED size value: {}", nextSize);
 253  
                         
 254  70
                         if (nextSize.length() == 0) {
 255  0
                                 break;
 256  
                         }
 257  
 
 258  
                         int nextSizeInt;
 259  
                         try {
 260  70
                                 nextSizeInt = Integer.parseInt(nextSize, 16);
 261  0
                         } catch (NumberFormatException e) {
 262  0
                                 throw new DecodeException("Failed to decode CHUNKED encoding", e);
 263  70
                         }
 264  
 
 265  70
                         ourLog.debug("Next CHUNKED size: {}", nextSizeInt);
 266  
 
 267  70
                         if (nextSizeInt < 0) {
 268  0
                                 throw new DecodeException("Received invalid octet count in chunked transfer encoding: " + nextSize);
 269  
                         }
 270  
 
 271  70
                         boolean trailing = false;
 272  70
                         if (nextSizeInt > 0) {
 273  40
                                 int totalRead = 0;
 274  40
                                 myLastStartedReading = System.currentTimeMillis();
 275  
                                 do {
 276  40
                                         int nextRead = Math.min(nextSizeInt, byteBuffer.length);
 277  40
                                         int bytesRead = theInputStream.read(byteBuffer, 0, nextRead);
 278  40
                                         if (bytesRead == -1) {
 279  0
                                                 ourLog.debug("Exception in readBytesChunked(InputStream): Reached EOF. Buffer has {} bytes", bos.size());
 280  0
                                                 throw new DecodeException("Reached EOF while reading in message chunk");
 281  
                                         }
 282  40
                                         if (bytesRead == 0 && totalRead < nextSizeInt) {
 283  0
                                                 pauseDuringTimedOutRead();
 284  
                                         }
 285  40
                                         totalRead += bytesRead;
 286  
 
 287  40
                                         if (ourLog.isTraceEnabled()) {
 288  0
                                                 ourLog.trace("Read {} byte chunk: {}", bytesRead, new String(byteBuffer, 0, bytesRead));
 289  
                                         }else {
 290  40
                                                 ourLog.debug("Read {} byte chunk", bytesRead);
 291  
                                         }
 292  
                                         
 293  40
                                         bos.write(byteBuffer, 0, bytesRead);
 294  
 
 295  40
                                 } while (totalRead < nextSizeInt);
 296  40
                         } else {
 297  30
                                 trailing = true;
 298  
                         }
 299  
 
 300  
                         // Try to read a trailing CRLF
 301  
                         int nextChar;
 302  70
                         boolean had13 = false;
 303  70
                         boolean had10 = false;
 304  
                         while (true) {
 305  
                                 try {
 306  140
                                         nextChar = theInputStream.read();
 307  140
                                         if (ourLog.isTraceEnabled()) {
 308  0
                                                 ourLog.trace("Read byte: " + (char)nextChar + " (" + nextChar + ")");
 309  
                                         }
 310  0
                                 } catch (SocketTimeoutException e) {
 311  0
                                         break;
 312  140
                                 }
 313  
 
 314  140
                                 if (nextChar == -1) {
 315  0
                                         break;
 316  140
                                 } else if (nextChar == 13) {
 317  70
                                         if (had13) {
 318  
                                                 /* 
 319  
                                                  * This is an attempt to be tolerant of people using the wrong
 320  
                                                  * end of line sequence (it should be CRLF), as is the 
 321  
                                                  * had10 below 
 322  
                                                  */
 323  0
                                                 trailing = true;
 324  
                                         }
 325  70
                                         had13 = true;
 326  70
                                         continue;
 327  70
                                 } else if (nextChar == 10) {
 328  70
                                         if (had10) {
 329  0
                                                 trailing = true;
 330  
                                         }
 331  
                                         break;
 332  
                                 } else {
 333  
                                         break;
 334  
                                 }
 335  
                         }
 336  
                         
 337  70
                         if (trailing) {
 338  30
                                 break;
 339  
                         }
 340  
 
 341  40
                 } // while
 342  
 
 343  30
                 return bos.toByteArray();
 344  
         }
 345  
 
 346  
         private void verifySignature() throws SignatureVerificationException, DecodeException {
 347  415
                 if (getSigner() != null && StringUtils.isBlank(mySignature)) {
 348  0
                         String mode = (this instanceof Hl7OverHttpRequestDecoder) ? "request" : "response";
 349  0
                         throw new SignatureVerificationException("No HL7 Signature found in " + mode);
 350  
                 }
 351  415
                 if (getSigner() != null) {
 352  
                         try {
 353  5
                                 getSigner().verify(myBytes, mySignature);
 354  0
                         } catch (SignatureFailureException e) {
 355  0
                                 throw new DecodeException("Failed to verify signature due to an error (signature may possibly be valid, but verification failed)", e);
 356  5
                         }
 357  
                 }
 358  415
         }
 359  
 
 360  
         public List<String> getConformanceProblems() {
 361  120
                 if (myConformanceProblems == null) {
 362  60
                         myConformanceProblems = new ArrayList<String>();
 363  
                 }
 364  120
                 return myConformanceProblems;
 365  
         }
 366  
 
 367  
         /**
 368  
          * @return Returns the content type associated with the message (e.g. application/hl7-v2)
 369  
          */
 370  
         public String getContentType() {
 371  600
                 return myContentType;
 372  
         }
 373  
 
 374  
         /**
 375  
          * @return the responseName
 376  
          */
 377  
         public String getResponseName() {
 378  0
                 return myResponseName;
 379  
         }
 380  
 
 381  
         /**
 382  
          * @return the responseStatus
 383  
          */
 384  
         public Integer getResponseStatus() {
 385  0
                 return myResponseStatus;
 386  
         }
 387  
 
 388  
         protected abstract String readActionLineAndDecode(InputStream theInputStream) throws IOException, NoMessageReceivedException, DecodeException;
 389  
 
 390  
         private byte[] readBytesNonChunked(InputStream theInputStream) throws IOException {
 391  385
                 ourLog.debug("Decoding message bytes using non-chunked encoding style");
 392  
 
 393  385
                 int length = myContentLength > 0 ? myContentLength : IOUtils.DEFAULT_BUFFER_SIZE;
 394  385
                 ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
 395  
 
 396  385
                 byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
 397  385
                 myLastStartedReading = System.currentTimeMillis();
 398  900
                 while ((myContentLength < 0 || bos.size() < myContentLength)) {
 399  515
                         if (myContentLength < 0) {
 400  
                                 try {
 401  0
                                         if (theInputStream.available() <= 0) {
 402  0
                                                 ourLog.trace("No more bytes available");
 403  0
                                                 break;
 404  
                                         }
 405  0
                                 } catch (IOException e) {
 406  0
                                         ourLog.debug("Received IOException while calling inputStream#available()", e);
 407  0
                                         throw e;
 408  0
                                 }
 409  
                         }
 410  
 
 411  
                         int max;
 412  515
                         if (myContentLength > 0) {
 413  515
                                 max = myContentLength - bos.size();
 414  515
                                 max = Math.min(max, buffer.length);
 415  
                         } else {
 416  0
                                 max = buffer.length;
 417  
                         }
 418  
                         
 419  
                         try {
 420  515
                                 int bytesRead = theInputStream.read(buffer, 0, max);
 421  510
                                 myLastStartedReading = System.currentTimeMillis();
 422  510
                                 if (bytesRead == -1) {
 423  0
                                         ourLog.trace("Read end of stream");
 424  0
                                         break;
 425  
                                 } else {
 426  510
                                         if (ourLog.isTraceEnabled()) {
 427  20
                                                 ourLog.trace("Read {} bytes from stream:\n{}", bytesRead, ByteUtils.formatBytesForLogging(bytesRead, 0, buffer));
 428  
                                         }
 429  
                                 }
 430  510
                                 bos.write(buffer, 0, bytesRead);
 431  5
                         } catch (SocketTimeoutException e) {
 432  5
                                 long elapsed = System.currentTimeMillis() - myLastStartedReading;
 433  5
                                 if (elapsed > myReadTimeout) {
 434  0
                                         throw e;
 435  
                                 } else {
 436  5
                                         ourLog.debug("Trying to read for {} / {}ms, going to keep trying", elapsed, myReadTimeout);
 437  
                                         try {
 438  5
                                                 Thread.sleep(100);
 439  0
                                         } catch (InterruptedException e1) {
 440  
                                                 // ignore
 441  5
                                         }
 442  
                                 }
 443  0
                         } catch (IOException e) {
 444  0
                                 ourLog.debug("Received IOException while calling inputStream#available()", e);
 445  0
                                 throw e;
 446  515
                         }
 447  515
                 }
 448  
 
 449  385
                 return bos.toByteArray();
 450  
         }
 451  
 
 452  
         /**
 453  
          * Read in the contents of the raw message from the input stream and decode
 454  
          * entire the message. This method assumes that the headers have been
 455  
          * provided using {@link #setHeaders(LinkedHashMap)}
 456  
          * 
 457  
          * @param theInputStream
 458  
          *            The inputstream to read the raw message from
 459  
          * @throws AuthorizationFailureException
 460  
          *             If the authorization check fails. This will only be thrown if
 461  
          *             this decoder is decoding a request message, and an
 462  
          *             authorization callback has been provided, and the
 463  
          *             authorization fails.
 464  
          * @throws DecodeException
 465  
          *             If the message can not be decoded for any reason
 466  
          * @throws IOException
 467  
          *             If there is a failure while reading from the inputstream
 468  
          * @throws SignatureVerificationException
 469  
          *             If the signature verification fails. This will only occur if
 470  
          *             {@link #setSigner(ca.uhn.hl7v2.hoh.sign.ISigner) a signer}
 471  
          *             has been provided.
 472  
          */
 473  
         public void readContentsFromInputStreamAndDecode(InputStream theInputStream) throws AuthorizationFailureException, DecodeException, IOException, SignatureVerificationException {
 474  50
                 verifyNotUsed();
 475  
 
 476  50
                 doReadContentsFromInputStreamAndDecode(theInputStream);
 477  50
         }
 478  
 
 479  
         protected String readFirstLine(InputStream theInputStream) throws IOException, NoMessageReceivedException {
 480  460
                 ourLog.trace("Entering readFirstLine(InputStream) with IS: {}", theInputStream);
 481  460
                 String retVal = readLine(theInputStream, true);
 482  365
                 ourLog.trace("Exiting readFirstLine(InputStream) with result: {}", retVal);
 483  365
                 return retVal;
 484  
         }
 485  
 
 486  
         /**
 487  
          * Note that if {@link #setPath(String)} is called, this method will assume
 488  
          * that the first line of the HTTP request has already been read from the
 489  
          * input stream. If {@link #setHeaders(java.util.LinkedHashMap)} has been
 490  
          * called, this method will assume that the HTTP headers have already been
 491  
          * read from the input stream as well as the double-LF (ASCII-10) that
 492  
          * proceeds the headers.
 493  
          * 
 494  
          * 
 495  
          * @param theInputStream
 496  
          *            The inputstream to read the raw message from
 497  
          * @throws AuthorizationFailureException
 498  
          *             If the authorization check fails. This will only be thrown if
 499  
          *             this decoder is decoding a request message, and an
 500  
          *             authorization callback has been provided, and the
 501  
          *             authorization fails.
 502  
          * @throws DecodeException
 503  
          *             If the message can not be decoded for any reason
 504  
          * @throws IOException
 505  
          *             If there is a failure while reading from the inputstream
 506  
          * @throws SignatureVerificationException
 507  
          *             If the signature verification fails. This will only occur if
 508  
          *             {@link #setSigner(ca.uhn.hl7v2.hoh.sign.ISigner) a signer}
 509  
          *             has been provided.
 510  
          */
 511  
         public void readHeadersAndContentsFromInputStreamAndDecode(InputStream theInputStream) throws IOException, DecodeException, NoMessageReceivedException, SignatureVerificationException {
 512  460
                 verifyNotUsed();
 513  
 
 514  460
                 String actionLine = readActionLineAndDecode(theInputStream);
 515  
 
 516  365
                 ourLog.debug("Read action line: {}", actionLine);
 517  
 
 518  365
                 if (getHeaders() == null) {
 519  365
                         setHeaders(new LinkedHashMap<String, String>());
 520  
 
 521  
                         while (true) {
 522  2250
                                 String nextLine = readLine(theInputStream);
 523  2250
                                 if (nextLine.length() == 0) {
 524  365
                                         break;
 525  
                                 }
 526  
 
 527  1885
                                 int colonIndex = nextLine.indexOf(':');
 528  1885
                                 if (colonIndex == -1) {
 529  0
                                         throw new DecodeException("Invalid HTTP header line detected. Value is: " + nextLine);
 530  
                                 }
 531  
 
 532  1885
                                 String key = nextLine.substring(0, colonIndex);
 533  1885
                                 String value = nextLine.substring(colonIndex + 1).trim();
 534  
                                 
 535  1885
                                 ourLog.debug("Read header {}={}", key,value);
 536  
                                 
 537  1885
                                 getHeaders().put(key, value);
 538  1885
                         }
 539  
                 }
 540  
 
 541  365
                 doReadContentsFromInputStreamAndDecode(theInputStream);
 542  
 
 543  365
         }
 544  
 
 545  
         private String readLine(InputStream theInputStream) throws IOException {
 546  
                 try {
 547  2320
                         return readLine(theInputStream, false);
 548  0
                 } catch (NoMessageReceivedException e) {
 549  0
                         throw new Error("Threw a NoMessageReceivedException. This should not happen.", e);
 550  
                 }
 551  
         }
 552  
 
 553  
         private String readLine(InputStream theInputStream, boolean theFirstLine) throws IOException, NoMessageReceivedException {
 554  
                 
 555  2780
                 myLastStartedReading = System.currentTimeMillis();
 556  
 
 557  2780
                 StringBuilder retVal = new StringBuilder();
 558  
                 while (true) {
 559  
 
 560  
                         int b;
 561  
                         try {
 562  85300
                                 b = theInputStream.read();
 563  85220
                                 if (ourLog.isTraceEnabled()) {
 564  4170
                                         ourLog.trace("Read byte: " + (char)b + " (" + b + ")");
 565  
                                 }
 566  50
                         } catch (SocketTimeoutException e) {
 567  50
                                 if (retVal.length() == 0 && theFirstLine) {
 568  30
                                         ourLog.trace("No message received, aborting readLine(InputStream, boolean)");
 569  30
                                         throw new NoMessageReceivedException();
 570  
                                 }
 571  20
                                 ourLog.trace("No message received in readLine(InputStream, boolean), going to wait and continue");
 572  20
                                 pauseDuringTimedOutRead();
 573  20
                                 continue;
 574  85220
                         }
 575  
 
 576  85220
                         if (b == 13) {
 577  2635
                                 continue;
 578  82585
                         } else if (b == 10) {
 579  2685
                                 break;
 580  79900
                         } else if (b == -1) {
 581  35
                                 ourLog.debug("Current read line is: {}", retVal);
 582  35
                                 ourLog.info("Read -1 from input stream, closing it");
 583  35
                                 theInputStream.close();
 584  35
                                 if (retVal.length() == 0) {
 585  35
                                         throw new SocketException("Received EOF from input stream");
 586  
                                 }
 587  
                                 break;
 588  79865
                         } else if (b < ' ') {
 589  45
                                 continue;
 590  
                         } else {
 591  79820
                                 retVal.append((char) b);
 592  
                         }
 593  79820
                 }
 594  
 
 595  2685
                 ourLog.debug("Current read line is: {}", retVal);
 596  
 
 597  2685
                 return WHITESPACE_PATTERN.matcher(retVal.toString()).replaceAll(" ").trim();
 598  
         }
 599  
 
 600  
         private void pauseDuringTimedOutRead() throws SocketTimeoutException {
 601  20
                 long elapsed = System.currentTimeMillis() - myLastStartedReading;
 602  20
                 if (elapsed > myReadTimeout) {
 603  0
                         ourLog.trace("Elapsed time of {} exceeds max {}, throwing SocketTimeoutException", elapsed, myReadTimeout);
 604  0
                         throw new SocketTimeoutException();
 605  
                 }
 606  
                 try {
 607  20
                         Thread.sleep(100);
 608  0
                 } catch (InterruptedException e1) {
 609  
                         // ignore
 610  20
                 }
 611  20
         }
 612  
 
 613  
         /**
 614  
          * Sets the number of milliseconds that the decoder will attempt to read
 615  
          * from an InputStream before timing out and throwing an exception
 616  
          */
 617  
         public void setReadTimeout(long theReadTimeout) {
 618  110
                 myReadTimeout = theReadTimeout;
 619  110
         }
 620  
 
 621  
         /**
 622  
          * @param theResponseName
 623  
          *            the responseName to set
 624  
          */
 625  
         public void setResponseName(String theResponseName) {
 626  145
                 myResponseName = theResponseName;
 627  145
         }
 628  
 
 629  
         /**
 630  
          * @param theResponseStatus
 631  
          *            the responseStatus to set
 632  
          */
 633  
         public void setResponseStatus(Integer theResponseStatus) {
 634  145
                 myResponseStatus = theResponseStatus;
 635  145
         }
 636  
 
 637  
 }