001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.server.connection;
031:
032: import com.caucho.log.Log;
033: import com.caucho.server.webapp.WebApp;
034: import com.caucho.util.L10N;
035: import com.caucho.vfs.ClientDisconnectException;
036: import com.caucho.vfs.WriteStream;
037:
038: import javax.servlet.ServletContext;
039: import java.io.IOException;
040: import java.io.OutputStream;
041: import java.util.*;
042: import java.util.logging.Level;
043: import java.util.logging.Logger;
044:
045: class ResponseStream extends ToByteResponseStream {
046: static final Logger log = Logger.getLogger(ResponseStream.class
047: .getName());
048:
049: static final L10N L = new L10N(ResponseStream.class);
050:
051: private static final int _tailChunkedLength = 7;
052: private static final byte[] _tailChunked = new byte[] { '\r', '\n',
053: '0', '\r', '\n', '\r', '\n' };
054:
055: private final AbstractHttpResponse _response;
056:
057: private WriteStream _next;
058:
059: private OutputStream _cacheStream;
060: private long _cacheMaxLength;
061: // used for the direct copy and caching
062: private int _bufferStartOffset;
063:
064: private boolean _chunkedEncoding;
065:
066: private int _bufferSize;
067: private boolean _disableAutoFlush;
068:
069: // bytes actually written
070: private int _contentLength;
071: // True for the first chunk
072: private boolean _isFirst;
073: private boolean _isDisconnected;
074: private boolean _isCommitted;
075:
076: private boolean _allowFlush = true;
077: private boolean _isHead = false;
078: private boolean _isClosed = false;
079:
080: private final byte[] _buffer = new byte[16];
081:
082: ResponseStream(AbstractHttpResponse response) {
083: _response = response;
084: }
085:
086: public void init(WriteStream next) {
087: _next = next;
088: }
089:
090: /**
091: * initializes the Response stream at the beginning of a request.
092: */
093: public void start() {
094: super .start();
095:
096: _chunkedEncoding = false;
097:
098: _contentLength = 0;
099: _allowFlush = true;
100: _disableAutoFlush = false;
101: _isClosed = false;
102: _isHead = false;
103: _cacheStream = null;
104: _isDisconnected = false;
105: _isCommitted = false;
106: _isFirst = true;
107: _bufferStartOffset = 0;
108: }
109:
110: /**
111: * Returns true for a Caucho response stream.
112: */
113: public boolean isCauchoResponseStream() {
114: return true;
115: }
116:
117: /**
118: * Sets the underlying cache stream for a cached request.
119: *
120: * @param cache the cache stream.
121: */
122: public void setByteCacheStream(OutputStream cacheStream) {
123: _cacheStream = cacheStream;
124:
125: CauchoRequest req = _response.getRequest();
126: WebApp app = req.getWebApp();
127: _cacheMaxLength = app.getCacheMaxLength();
128: }
129:
130: /**
131: * Response stream is a writable stream.
132: */
133: public boolean canWrite() {
134: return true;
135: }
136:
137: void setFlush(boolean flush) {
138: _allowFlush = flush;
139: }
140:
141: public void setAutoFlush(boolean isAutoFlush) {
142: setDisableAutoFlush(!isAutoFlush);
143: }
144:
145: void setDisableAutoFlush(boolean disable) {
146: _disableAutoFlush = disable;
147: }
148:
149: public void setHead() {
150: _isHead = true;
151: _bufferSize = 0;
152: }
153:
154: public boolean isHead() {
155: return _isHead;
156: }
157:
158: public int getContentLength() {
159: return _contentLength;
160: }
161:
162: public void setBufferSize(int size) {
163: if (isCommitted())
164: throw new IllegalStateException(L
165: .l("Buffer size cannot be set after commit"));
166:
167: super .setBufferSize(size);
168: }
169:
170: public boolean isCommitted() {
171: // jsp/17ec
172: return _isCommitted || _isClosed;
173: }
174:
175: public void clear() throws IOException {
176: clearBuffer();
177:
178: if (_isCommitted)
179: throw new IOException(L
180: .l("can't clear response after writing headers"));
181: }
182:
183: public void clearBuffer() {
184: super .clearBuffer();
185:
186: if (!_isCommitted) {
187: // jsp/15la
188: _isFirst = true;
189: _bufferStartOffset = 0;
190: _response.setHeaderWritten(false);
191: }
192:
193: _next.setBufferOffset(_bufferStartOffset);
194: }
195:
196: /**
197: * Clear the closed state, because of the NOT_MODIFIED
198: */
199: public void clearClosed() {
200: _isClosed = false;
201: }
202:
203: private void writeHeaders(int length) throws IOException {
204: _chunkedEncoding = _response.writeHeaders(_next, length);
205: }
206:
207: /**
208: * Returns the byte buffer.
209: */
210: public byte[] getBuffer() throws IOException {
211: flushBuffer();
212:
213: return _next.getBuffer();
214: }
215:
216: /**
217: * Returns the byte offset.
218: */
219: public int getBufferOffset() throws IOException {
220: byte[] buffer;
221: int offset;
222:
223: flushBuffer();
224:
225: offset = _next.getBufferOffset();
226:
227: if (!_chunkedEncoding) {
228: _bufferStartOffset = offset;
229: return offset;
230: } else if (_bufferStartOffset > 0) {
231: return offset;
232: }
233:
234: // chunked allocates 8 bytes for the chunk header
235: buffer = _next.getBuffer();
236: if (buffer.length - offset < 8) {
237: _isCommitted = true;
238: _next.flushBuffer();
239:
240: buffer = _next.getBuffer();
241: offset = _next.getBufferOffset();
242: }
243:
244: _bufferStartOffset = offset + 8;
245: _next.setBufferOffset(offset + 8);
246:
247: return _bufferStartOffset;
248: }
249:
250: /**
251: * Sets the next buffer
252: */
253: public byte[] nextBuffer(int offset) throws IOException {
254: if (_isClosed)
255: return _next.getBuffer();
256:
257: _isCommitted = true;
258:
259: int startOffset = _bufferStartOffset;
260: _bufferStartOffset = 0;
261:
262: int length = offset - startOffset;
263: long lengthHeader = _response.getContentLengthHeader();
264:
265: if (lengthHeader > 0 && lengthHeader < _contentLength + length) {
266: lengthException(_next.getBuffer(), startOffset, length,
267: lengthHeader);
268:
269: length = (int) (lengthHeader - _contentLength);
270: offset = startOffset + length;
271: }
272:
273: _contentLength += length;
274:
275: try {
276: if (_isHead) {
277: return _next.getBuffer();
278: } else if (_chunkedEncoding) {
279: if (length == 0)
280: throw new IllegalStateException();
281:
282: byte[] buffer = _next.getBuffer();
283:
284: writeChunk(buffer, startOffset, length);
285:
286: buffer = _next.nextBuffer(offset);
287:
288: if (log.isLoggable(Level.FINE))
289: log.fine(dbgId() + "write-chunk(" + offset + ")");
290:
291: _bufferStartOffset = 8 + _next.getBufferOffset();
292: _next.setBufferOffset(_bufferStartOffset);
293:
294: return buffer;
295: } else {
296: if (_cacheStream != null)
297: writeCache(_next.getBuffer(), startOffset, length);
298:
299: byte[] buffer = _next.nextBuffer(offset);
300:
301: if (log.isLoggable(Level.FINE))
302: log.fine(dbgId() + "write-chunk(" + offset + ")");
303:
304: return buffer;
305: }
306: } catch (ClientDisconnectException e) {
307: _response.killCache();
308:
309: if (_response.isIgnoreClientDisconnect()) {
310: _isDisconnected = true;
311: return _next.getBuffer();
312: } else
313: throw e;
314: } catch (IOException e) {
315: _response.killCache();
316:
317: throw e;
318: }
319: }
320:
321: /**
322: * Sets the byte offset.
323: */
324: public void setBufferOffset(int offset) throws IOException {
325: if (_isClosed)
326: return;
327:
328: int startOffset = _bufferStartOffset;
329: if (offset == startOffset)
330: return;
331:
332: int length = offset - startOffset;
333: long lengthHeader = _response.getContentLengthHeader();
334:
335: if (lengthHeader > 0 && lengthHeader < _contentLength + length) {
336: lengthException(_next.getBuffer(), startOffset, length,
337: lengthHeader);
338:
339: length = (int) (lengthHeader - _contentLength);
340: offset = startOffset + length;
341: }
342:
343: _contentLength += length;
344:
345: if (_cacheStream != null && !_chunkedEncoding) {
346: _bufferStartOffset = offset;
347: writeCache(_next.getBuffer(), startOffset, length);
348: }
349:
350: if (log.isLoggable(Level.FINE))
351: log.fine(dbgId() + "write-chunk(" + length + ")");
352:
353: if (!_isHead) {
354: _next.setBufferOffset(offset);
355: }
356: }
357:
358: /**
359: * Writes the next chunk of data to the response stream.
360: *
361: * @param buf the buffer containing the data
362: * @param offset start offset into the buffer
363: * @param length length of the data in the buffer
364: */
365: protected void writeNext(byte[] buf, int offset, int length,
366: boolean isFinished) throws IOException {
367: try {
368: if (_isClosed)
369: return;
370:
371: if (_disableAutoFlush && !isFinished)
372: throw new IOException(L
373: .l("auto-flushing has been disabled"));
374:
375: boolean isFirst = _isFirst;
376: _isFirst = false;
377:
378: if (!isFirst) {
379: } else if (isFinished)
380: writeHeaders(getBufferLength());
381: else
382: writeHeaders(-1);
383:
384: int bufferStart = _bufferStartOffset;
385: int bufferOffset = _next.getBufferOffset();
386:
387: // server/05e2
388: if (length == 0 && !isFinished
389: && bufferStart == bufferOffset)
390: return;
391:
392: long contentLengthHeader = _response
393: .getContentLengthHeader();
394: // Can't write beyond the content length
395: if (0 < contentLengthHeader
396: && contentLengthHeader < length + _contentLength) {
397: if (lengthException(buf, offset, length,
398: contentLengthHeader))
399: return;
400:
401: length = (int) (contentLengthHeader - _contentLength);
402: }
403:
404: if (_next != null && !_isHead) {
405: if (length > 0 && log.isLoggable(Level.FINE)) {
406: log.fine(dbgId() + "write-chunk(" + length + ")");
407: }
408:
409: if (!_chunkedEncoding) {
410: byte[] nextBuffer = _next.getBuffer();
411: int nextOffset = _next.getBufferOffset();
412:
413: if (nextOffset + length < nextBuffer.length) {
414: System.arraycopy(buf, offset, nextBuffer,
415: nextOffset, length);
416: _next.setBufferOffset(nextOffset + length);
417: } else {
418: _isCommitted = true;
419: _next.write(buf, offset, length);
420:
421: if (log.isLoggable(Level.FINE))
422: log.fine(dbgId() + "write-data("
423: + _tailChunkedLength + ")");
424: }
425:
426: if (_cacheStream != null)
427: writeCache(buf, offset, length);
428: } else {
429: byte[] buffer = _next.getBuffer();
430: int writeLength = length;
431:
432: if (bufferStart == 0 && writeLength > 0) {
433: bufferStart = bufferOffset + 8;
434: bufferOffset = bufferStart;
435: }
436:
437: while (writeLength > 0) {
438: int sublen = buffer.length - bufferOffset;
439:
440: if (writeLength < sublen)
441: sublen = writeLength;
442:
443: System.arraycopy(buf, offset, buffer,
444: bufferOffset, sublen);
445:
446: writeLength -= sublen;
447: offset += sublen;
448: bufferOffset += sublen;
449:
450: if (writeLength > 0) {
451: int delta = bufferOffset - bufferStart;
452: writeChunk(buffer, bufferStart, delta);
453:
454: _isCommitted = true;
455: buffer = _next.nextBuffer(bufferOffset);
456:
457: if (log.isLoggable(Level.FINE))
458: log.fine(dbgId() + "write-chunk("
459: + bufferOffset + ")");
460:
461: bufferStart = _next.getBufferOffset() + 8;
462: bufferOffset = bufferStart;
463: }
464: }
465:
466: _next.setBufferOffset(bufferOffset);
467: _bufferStartOffset = bufferStart;
468: }
469: }
470:
471: if (!_isDisconnected)
472: _contentLength += length;
473: } catch (ClientDisconnectException e) {
474: // server/183c
475: _response.killCache();
476:
477: if (_response.isIgnoreClientDisconnect())
478: _isDisconnected = true;
479: else {
480: throw e;
481: }
482: }
483: }
484:
485: private boolean lengthException(byte[] buf, int offset, int length,
486: long contentLengthHeader) {
487: if (_isDisconnected || _isHead || _isClosed) {
488: } else if (contentLengthHeader < _contentLength) {
489: CauchoRequest request = _response.getRequest();
490: ServletContext app = request.getWebApp();
491:
492: Exception exn = new IllegalStateException(
493: L
494: .l(
495: "{0}: tried to write {1} bytes with content-length {2}.",
496: request.getRequestURL(),
497: "" + (length + _contentLength), ""
498: + contentLengthHeader));
499:
500: if (app != null)
501: app.log(exn.getMessage(), exn);
502: else
503: exn.printStackTrace();
504:
505: return false;
506: }
507:
508: for (int i = (int) (offset + contentLengthHeader - _contentLength); i < offset
509: + length; i++) {
510: int ch = buf[i];
511:
512: if (ch != '\r' && ch != '\n' && ch != ' ' && ch != '\t') {
513: CauchoRequest request = _response.getRequest();
514: ServletContext app = request.getWebApp();
515: String graph = "";
516:
517: if (Character.isLetterOrDigit((char) ch))
518: graph = "'" + (char) ch + "', ";
519:
520: Exception exn = new IllegalStateException(
521: L
522: .l(
523: "{0}: tried to write {1} bytes with content-length {2} (At {3}char={4}).",
524: request.getRequestURL(),
525: "" + (length + _contentLength),
526: "" + contentLengthHeader,
527: graph, "" + ch));
528:
529: if (app != null)
530: app.log(exn.toString(), exn);
531: else
532: exn.printStackTrace();
533: break;
534: }
535: }
536:
537: length = (int) (contentLengthHeader - _contentLength);
538: return (length <= 0);
539: }
540:
541: public void flushBuffer() throws IOException {
542: super .flushBuffer();
543:
544: _isCommitted = true;
545: }
546:
547: /**
548: * Flushes the buffered response to the output stream.
549: */
550: public void flush() throws IOException {
551: try {
552: _disableAutoFlush = false;
553: _isCommitted = true;
554:
555: if (_allowFlush && !_isClosed) {
556: flushBuffer();
557:
558: if (_chunkedEncoding) {
559: int bufferStart = _bufferStartOffset;
560: _bufferStartOffset = 0;
561:
562: if (bufferStart > 0) {
563: int bufferOffset = _next.getBufferOffset();
564:
565: if (bufferStart != bufferOffset) {
566: writeChunk(_next.getBuffer(), bufferStart,
567: bufferOffset - bufferStart);
568: } else
569: _next.setBufferOffset(bufferStart - 8);
570: }
571: } else {
572: // jsp/01cf
573: _bufferStartOffset = 0;
574: }
575:
576: if (_next != null)
577: _next.flush();
578: }
579: } catch (ClientDisconnectException e) {
580: if (_response.isIgnoreClientDisconnect())
581: _isDisconnected = true;
582: else
583: throw e;
584: }
585: }
586:
587: /**
588: * Flushes the buffered response to the output stream.
589: */
590: public void flushByte() throws IOException {
591: flush();
592: }
593:
594: /**
595: * Flushes the buffered response to the writer.
596: */
597: public void flushChar() throws IOException {
598: flush();
599: }
600:
601: /**
602: * Flushes the buffered response to the output stream.
603: */
604: /*
605: public void flushBuffer()
606: throws IOException
607: {
608: super.flushBuffer();
609:
610: // jsp/15la
611: // _isCommitted = true;
612: }
613: */
614:
615: /**
616: * Complete the request.
617: */
618: public void finish() throws IOException {
619: boolean isClosed = _isClosed;
620:
621: if (_next == null || isClosed) {
622: _isClosed = true;
623: return;
624: }
625:
626: _disableAutoFlush = false;
627:
628: flushCharBuffer();
629:
630: _isFinished = true;
631: _allowFlush = true;
632:
633: flushBuffer();
634:
635: int bufferStart = _bufferStartOffset;
636: _bufferStartOffset = 0;
637: _isClosed = true;
638:
639: // flushBuffer can force 304 and then a cache write which would
640: // complete the finish.
641: if (isClosed || _next == null) {
642: return;
643: }
644:
645: try {
646: if (_chunkedEncoding) {
647: int bufferOffset = _next.getBufferOffset();
648:
649: if (bufferStart > 0 && bufferOffset != bufferStart) {
650: byte[] buffer = _next.getBuffer();
651:
652: writeChunk(buffer, bufferStart, bufferOffset
653: - bufferStart);
654: } else {
655: // server/05b3
656: _next.setBufferOffset(0);
657: }
658:
659: _isCommitted = true;
660:
661: ArrayList<String> footerKeys = _response._footerKeys;
662:
663: if (footerKeys.size() == 0)
664: _next.write(_tailChunked, 0, _tailChunkedLength);
665: else {
666: ArrayList<String> footerValues = _response._footerValues;
667:
668: _next.print("\r\n0\r\n");
669:
670: for (int i = 0; i < footerKeys.size(); i++) {
671: _next.print(footerKeys.get(i));
672: _next.print(": ");
673: _next.print(footerValues.get(i));
674: _next.print("\r\n");
675: }
676:
677: _next.print("\r\n");
678: }
679:
680: if (log.isLoggable(Level.FINE))
681: log.fine(dbgId() + "write-chunk("
682: + _tailChunkedLength + ")");
683: }
684:
685: CauchoRequest req = _response.getRequest();
686: if (!req.allowKeepalive()) {
687: if (log.isLoggable(Level.FINE)) {
688: log.fine(dbgId() + "close stream");
689: }
690:
691: _next.close();
692: }
693: /*
694: else if (flush) {
695: //_next.flush();
696: _next.flushBuffer();
697: }
698: */
699: } catch (ClientDisconnectException e) {
700: if (_response.isIgnoreClientDisconnect())
701: _isDisconnected = true;
702: else
703: throw e;
704: }
705: }
706:
707: /**
708: * Fills the chunk header.
709: */
710: private void writeChunk(byte[] buffer, int start, int length)
711: throws IOException {
712: buffer[start - 8] = (byte) '\r';
713: buffer[start - 7] = (byte) '\n';
714: buffer[start - 6] = hexDigit(length >> 12);
715: buffer[start - 5] = hexDigit(length >> 8);
716: buffer[start - 4] = hexDigit(length >> 4);
717: buffer[start - 3] = hexDigit(length);
718: buffer[start - 2] = (byte) '\r';
719: buffer[start - 1] = (byte) '\n';
720:
721: if (_cacheStream != null)
722: writeCache(buffer, start, length);
723: }
724:
725: /**
726: * Returns the hex digit for the value.
727: */
728: private static byte hexDigit(int value) {
729: value &= 0xf;
730:
731: if (value <= 9)
732: return (byte) ('0' + value);
733: else
734: return (byte) ('a' + value - 10);
735: }
736:
737: private void writeCache(byte[] buf, int offset, int length)
738: throws IOException {
739: if (length == 0)
740: return;
741:
742: if (_cacheMaxLength < _contentLength) {
743: _cacheStream = null;
744: _response.killCache();
745: } else {
746: _cacheStream.write(buf, offset, length);
747: }
748: }
749:
750: private String dbgId() {
751: Object request = _response.getRequest();
752:
753: if (request instanceof AbstractHttpRequest) {
754: AbstractHttpRequest req = (AbstractHttpRequest) request;
755:
756: return req.dbgId();
757: } else
758: return "inc";
759: }
760:
761: /**
762: * Closes the stream.
763: */
764: public void close() throws IOException {
765: finish();
766: }
767: }
|