001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027 028package org.apache.http.impl.io; 029 030import java.io.IOException; 031import java.io.InputStream; 032import java.nio.ByteBuffer; 033import java.nio.CharBuffer; 034import java.nio.charset.CharsetDecoder; 035import java.nio.charset.CoderResult; 036 037import org.apache.http.MessageConstraintException; 038import org.apache.http.config.MessageConstraints; 039import org.apache.http.io.BufferInfo; 040import org.apache.http.io.HttpTransportMetrics; 041import org.apache.http.io.SessionInputBuffer; 042import org.apache.http.protocol.HTTP; 043import org.apache.http.util.Args; 044import org.apache.http.util.Asserts; 045import org.apache.http.util.ByteArrayBuffer; 046import org.apache.http.util.CharArrayBuffer; 047 048/** 049 * Abstract base class for session input buffers that stream data from 050 * an arbitrary {@link InputStream}. This class buffers input data in 051 * an internal byte array for optimal input performance. 052 * <p> 053 * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this 054 * class treat a lone LF as valid line delimiters in addition to CR-LF required 055 * by the HTTP specification. 056 * 057 * @since 4.3 058 */ 059public class SessionInputBufferImpl implements SessionInputBuffer, BufferInfo { 060 061 private final HttpTransportMetricsImpl metrics; 062 private final byte[] buffer; 063 private final ByteArrayBuffer linebuffer; 064 private final int minChunkLimit; 065 private final MessageConstraints constraints; 066 private final CharsetDecoder decoder; 067 068 private InputStream instream; 069 private int bufferpos; 070 private int bufferlen; 071 private CharBuffer cbuf; 072 073 /** 074 * Creates new instance of SessionInputBufferImpl. 075 * 076 * @param metrics HTTP transport metrics. 077 * @param buffersize buffer size. Must be a positive number. 078 * @param minChunkLimit size limit below which data chunks should be buffered in memory 079 * in order to minimize native method invocations on the underlying network socket. 080 * The optimal value of this parameter can be platform specific and defines a trade-off 081 * between performance of memory copy operations and that of native method invocation. 082 * If negative default chunk limited will be used. 083 * @param constraints Message constraints. If {@code null} 084 * {@link MessageConstraints#DEFAULT} will be used. 085 * @param chardecoder chardecoder to be used for decoding HTTP protocol elements. 086 * If {@code null} simple type cast will be used for byte to char conversion. 087 */ 088 public SessionInputBufferImpl( 089 final HttpTransportMetricsImpl metrics, 090 final int buffersize, 091 final int minChunkLimit, 092 final MessageConstraints constraints, 093 final CharsetDecoder chardecoder) { 094 Args.notNull(metrics, "HTTP transport metrcis"); 095 Args.positive(buffersize, "Buffer size"); 096 this.metrics = metrics; 097 this.buffer = new byte[buffersize]; 098 this.bufferpos = 0; 099 this.bufferlen = 0; 100 this.minChunkLimit = minChunkLimit >= 0 ? minChunkLimit : 512; 101 this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT; 102 this.linebuffer = new ByteArrayBuffer(buffersize); 103 this.decoder = chardecoder; 104 } 105 106 public SessionInputBufferImpl( 107 final HttpTransportMetricsImpl metrics, 108 final int buffersize) { 109 this(metrics, buffersize, buffersize, null, null); 110 } 111 112 public void bind(final InputStream instream) { 113 this.instream = instream; 114 } 115 116 public boolean isBound() { 117 return this.instream != null; 118 } 119 120 @Override 121 public int capacity() { 122 return this.buffer.length; 123 } 124 125 @Override 126 public int length() { 127 return this.bufferlen - this.bufferpos; 128 } 129 130 @Override 131 public int available() { 132 return capacity() - length(); 133 } 134 135 private int streamRead(final byte[] b, final int off, final int len) throws IOException { 136 Asserts.notNull(this.instream, "Input stream"); 137 return this.instream.read(b, off, len); 138 } 139 140 public int fillBuffer() throws IOException { 141 // compact the buffer if necessary 142 if (this.bufferpos > 0) { 143 final int len = this.bufferlen - this.bufferpos; 144 if (len > 0) { 145 System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len); 146 } 147 this.bufferpos = 0; 148 this.bufferlen = len; 149 } 150 final int l; 151 final int off = this.bufferlen; 152 final int len = this.buffer.length - off; 153 l = streamRead(this.buffer, off, len); 154 if (l == -1) { 155 return -1; 156 } else { 157 this.bufferlen = off + l; 158 this.metrics.incrementBytesTransferred(l); 159 return l; 160 } 161 } 162 163 public boolean hasBufferedData() { 164 return this.bufferpos < this.bufferlen; 165 } 166 167 public void clear() { 168 this.bufferpos = 0; 169 this.bufferlen = 0; 170 } 171 172 @Override 173 public int read() throws IOException { 174 int noRead; 175 while (!hasBufferedData()) { 176 noRead = fillBuffer(); 177 if (noRead == -1) { 178 return -1; 179 } 180 } 181 return this.buffer[this.bufferpos++] & 0xff; 182 } 183 184 @Override 185 public int read(final byte[] b, final int off, final int len) throws IOException { 186 if (b == null) { 187 return 0; 188 } 189 if (hasBufferedData()) { 190 final int chunk = Math.min(len, this.bufferlen - this.bufferpos); 191 System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); 192 this.bufferpos += chunk; 193 return chunk; 194 } 195 // If the remaining capacity is big enough, read directly from the 196 // underlying input stream bypassing the buffer. 197 if (len > this.minChunkLimit) { 198 final int read = streamRead(b, off, len); 199 if (read > 0) { 200 this.metrics.incrementBytesTransferred(read); 201 } 202 return read; 203 } else { 204 // otherwise read to the buffer first 205 while (!hasBufferedData()) { 206 final int noRead = fillBuffer(); 207 if (noRead == -1) { 208 return -1; 209 } 210 } 211 final int chunk = Math.min(len, this.bufferlen - this.bufferpos); 212 System.arraycopy(this.buffer, this.bufferpos, b, off, chunk); 213 this.bufferpos += chunk; 214 return chunk; 215 } 216 } 217 218 @Override 219 public int read(final byte[] b) throws IOException { 220 if (b == null) { 221 return 0; 222 } 223 return read(b, 0, b.length); 224 } 225 226 /** 227 * Reads a complete line of characters up to a line delimiter from this 228 * session buffer into the given line buffer. The number of chars actually 229 * read is returned as an integer. The line delimiter itself is discarded. 230 * If no char is available because the end of the stream has been reached, 231 * the value {@code -1} is returned. This method blocks until input 232 * data is available, end of file is detected, or an exception is thrown. 233 * <p> 234 * This method treats a lone LF as a valid line delimiters in addition 235 * to CR-LF required by the HTTP specification. 236 * 237 * @param charbuffer the line buffer. 238 * @return one line of characters 239 * @throws IOException if an I/O error occurs. 240 */ 241 @Override 242 public int readLine(final CharArrayBuffer charbuffer) throws IOException { 243 Args.notNull(charbuffer, "Char array buffer"); 244 final int maxLineLen = this.constraints.getMaxLineLength(); 245 int noRead = 0; 246 boolean retry = true; 247 while (retry) { 248 // attempt to find end of line (LF) 249 int pos = -1; 250 for (int i = this.bufferpos; i < this.bufferlen; i++) { 251 if (this.buffer[i] == HTTP.LF) { 252 pos = i; 253 break; 254 } 255 } 256 257 if (maxLineLen > 0) { 258 final int currentLen = this.linebuffer.length() 259 + (pos >= 0 ? pos : this.bufferlen) - this.bufferpos; 260 if (currentLen >= maxLineLen) { 261 throw new MessageConstraintException("Maximum line length limit exceeded"); 262 } 263 } 264 265 if (pos != -1) { 266 // end of line found. 267 if (this.linebuffer.isEmpty()) { 268 // the entire line is preset in the read buffer 269 return lineFromReadBuffer(charbuffer, pos); 270 } 271 retry = false; 272 final int len = pos + 1 - this.bufferpos; 273 this.linebuffer.append(this.buffer, this.bufferpos, len); 274 this.bufferpos = pos + 1; 275 } else { 276 // end of line not found 277 if (hasBufferedData()) { 278 final int len = this.bufferlen - this.bufferpos; 279 this.linebuffer.append(this.buffer, this.bufferpos, len); 280 this.bufferpos = this.bufferlen; 281 } 282 noRead = fillBuffer(); 283 if (noRead == -1) { 284 retry = false; 285 } 286 } 287 } 288 if (noRead == -1 && this.linebuffer.isEmpty()) { 289 // indicate the end of stream 290 return -1; 291 } 292 return lineFromLineBuffer(charbuffer); 293 } 294 295 /** 296 * Reads a complete line of characters up to a line delimiter from this 297 * session buffer. The line delimiter itself is discarded. If no char is 298 * available because the end of the stream has been reached, 299 * {@code null} is returned. This method blocks until input data is 300 * available, end of file is detected, or an exception is thrown. 301 * <p> 302 * This method treats a lone LF as a valid line delimiters in addition 303 * to CR-LF required by the HTTP specification. 304 * 305 * @return HTTP line as a string 306 * @throws IOException if an I/O error occurs. 307 */ 308 private int lineFromLineBuffer(final CharArrayBuffer charbuffer) 309 throws IOException { 310 // discard LF if found 311 int len = this.linebuffer.length(); 312 if (len > 0) { 313 if (this.linebuffer.byteAt(len - 1) == HTTP.LF) { 314 len--; 315 } 316 // discard CR if found 317 if (len > 0) { 318 if (this.linebuffer.byteAt(len - 1) == HTTP.CR) { 319 len--; 320 } 321 } 322 } 323 if (this.decoder == null) { 324 charbuffer.append(this.linebuffer, 0, len); 325 } else { 326 final ByteBuffer bbuf = ByteBuffer.wrap(this.linebuffer.buffer(), 0, len); 327 len = appendDecoded(charbuffer, bbuf); 328 } 329 this.linebuffer.clear(); 330 return len; 331 } 332 333 private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int position) 334 throws IOException { 335 int pos = position; 336 final int off = this.bufferpos; 337 int len; 338 this.bufferpos = pos + 1; 339 if (pos > off && this.buffer[pos - 1] == HTTP.CR) { 340 // skip CR if found 341 pos--; 342 } 343 len = pos - off; 344 if (this.decoder == null) { 345 charbuffer.append(this.buffer, off, len); 346 } else { 347 final ByteBuffer bbuf = ByteBuffer.wrap(this.buffer, off, len); 348 len = appendDecoded(charbuffer, bbuf); 349 } 350 return len; 351 } 352 353 private int appendDecoded( 354 final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException { 355 if (!bbuf.hasRemaining()) { 356 return 0; 357 } 358 if (this.cbuf == null) { 359 this.cbuf = CharBuffer.allocate(1024); 360 } 361 this.decoder.reset(); 362 int len = 0; 363 while (bbuf.hasRemaining()) { 364 final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true); 365 len += handleDecodingResult(result, charbuffer, bbuf); 366 } 367 final CoderResult result = this.decoder.flush(this.cbuf); 368 len += handleDecodingResult(result, charbuffer, bbuf); 369 this.cbuf.clear(); 370 return len; 371 } 372 373 private int handleDecodingResult( 374 final CoderResult result, 375 final CharArrayBuffer charbuffer, 376 final ByteBuffer bbuf) throws IOException { 377 if (result.isError()) { 378 result.throwException(); 379 } 380 this.cbuf.flip(); 381 final int len = this.cbuf.remaining(); 382 while (this.cbuf.hasRemaining()) { 383 charbuffer.append(this.cbuf.get()); 384 } 385 this.cbuf.compact(); 386 return len; 387 } 388 389 @Override 390 public String readLine() throws IOException { 391 final CharArrayBuffer charbuffer = new CharArrayBuffer(64); 392 final int l = readLine(charbuffer); 393 if (l != -1) { 394 return charbuffer.toString(); 395 } else { 396 return null; 397 } 398 } 399 400 @Override 401 public boolean isDataAvailable(final int timeout) throws IOException { 402 return hasBufferedData(); 403 } 404 405 @Override 406 public HttpTransportMetrics getMetrics() { 407 return this.metrics; 408 } 409 410}