/* * 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.catalina.connector; import java.io.IOException; import java.io.Writer; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.catalina.Globals; import org.apache.coyote.ActionCode; import org.apache.coyote.Response; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.C2BConverter; /** * The buffer used by Tomcat response. This is a derivative of the Tomcat 3.3 * OutputBuffer, with the removal of some of the state handling (which in * Coyote is mostly the Processor's responsability). * * @author Costin Manolache * @author Remy Maucherat */ public class OutputBuffer extends Writer implements ByteChunk.ByteOutputChannel { // -------------------------------------------------------------- Constants public static final String DEFAULT_ENCODING = org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING; public static final int DEFAULT_BUFFER_SIZE = 8*1024; // ----------------------------------------------------- Instance Variables /** * The byte buffer. */ private ByteChunk bb; /** * State of the output buffer. */ private boolean initial = true; /** * Number of bytes written. */ private long bytesWritten = 0; /** * Number of chars written. */ private long charsWritten = 0; /** * Flag which indicates if the output buffer is closed. */ private boolean closed = false; /** * Do a flush on the next operation. */ private boolean doFlush = false; /** * Byte chunk used to output bytes. */ private ByteChunk outputChunk = new ByteChunk(); /** * Encoding to use. */ private String enc; /** * Encoder is set. */ private boolean gotEnc = false; /** * List of encoders. */ protected final Map encoders = new ConcurrentHashMap(); /** * Current char to byte converter. */ protected C2BConverter conv; /** * Associated Coyote response. */ private Response coyoteResponse; /** * Suspended flag. All output bytes will be swallowed if this is true. */ private boolean suspended = false; // ----------------------------------------------------------- Constructors /** * Default constructor. Allocate the buffer with the default buffer size. */ public OutputBuffer() { this(DEFAULT_BUFFER_SIZE); } /** * Alternate constructor which allows specifying the initial buffer size. * * @param size Buffer size to use */ public OutputBuffer(int size) { bb = new ByteChunk(size); bb.setLimit(size); bb.setByteOutputChannel(this); } // ------------------------------------------------------------- Properties /** * Associated Coyote response. * * @param coyoteResponse Associated Coyote response */ public void setResponse(Response coyoteResponse) { this.coyoteResponse = coyoteResponse; } /** * Get associated Coyote response. * * @return the associated Coyote response */ public Response getResponse() { return this.coyoteResponse; } /** * Is the response output suspended ? * * @return suspended flag value */ public boolean isSuspended() { return this.suspended; } /** * Set the suspended flag. * * @param suspended New suspended flag value */ public void setSuspended(boolean suspended) { this.suspended = suspended; } /** * Is the response output closed ? * * @return closed flag value */ public boolean isClosed() { return this.closed; } // --------------------------------------------------------- Public Methods /** * Recycle the output buffer. */ public void recycle() { initial = true; bytesWritten = 0; charsWritten = 0; bb.recycle(); closed = false; doFlush = false; suspended = false; if (conv!= null) { conv.recycle(); } gotEnc = false; enc = null; } /** * Clear cached encoders (to save memory for Comet requests). */ public void clearEncoders() { encoders.clear(); } /** * Close the output buffer. This tries to calculate the response size if * the response has not been committed yet. * * @throws IOException An underlying IOException occurred */ public void close() throws IOException { if (closed) return; if (suspended) return; if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1) && !coyoteResponse.getRequest().method().equals("HEAD")) { // If this didn't cause a commit of the response, the final content // length can be calculated. Only do this if this is not a HEAD // request since in that case no body should have been written and // setting a value of zero here will result in an explicit content // length of zero being set on the response. if (!coyoteResponse.isCommitted()) { coyoteResponse.setContentLength(bb.getLength()); } } doFlush(false); closed = true; // The request should have been completely read by the time the response // is closed. Further reads of the input a) are pointless and b) really // confuse AJP (bug 50189) so close the input buffer to prevent them. Request req = (Request) coyoteResponse.getRequest().getNote( CoyoteAdapter.ADAPTER_NOTES); req.inputBuffer.close(); coyoteResponse.finish(); } /** * Flush bytes or chars contained in the buffer. * * @throws IOException An underlying IOException occurred */ public void flush() throws IOException { doFlush(true); } /** * Flush bytes or chars contained in the buffer. * * @throws IOException An underlying IOException occurred */ protected void doFlush(boolean realFlush) throws IOException { if (suspended) return; try { doFlush = true; if (initial) { coyoteResponse.sendHeaders(); initial = false; } if (bb.getLength() > 0) { bb.flushBuffer(); } } finally { doFlush = false; } if (realFlush) { coyoteResponse.action(ActionCode.ACTION_CLIENT_FLUSH, coyoteResponse); // If some exception occurred earlier, or if some IOE occurred // here, notify the servlet with an IOE if (coyoteResponse.isExceptionPresent()) { throw new ClientAbortException (coyoteResponse.getErrorException()); } } } // ------------------------------------------------- Bytes Handling Methods /** * Sends the buffer data to the client output, checking the * state of Response and calling the right interceptors. * * @param buf Byte buffer to be written to the response * @param off Offset * @param cnt Length * * @throws IOException An underlying IOException occurred */ public void realWriteBytes(byte buf[], int off, int cnt) throws IOException { if (closed) return; if (coyoteResponse == null) return; // If we really have something to write if (cnt > 0) { // real write to the adapter outputChunk.setBytes(buf, off, cnt); try { coyoteResponse.doWrite(outputChunk); } catch (IOException e) { // An IOException on a write is almost always due to // the remote client aborting the request. Wrap this // so that it can be handled better by the error dispatcher. throw new ClientAbortException(e); } } } public void write(byte b[], int off, int len) throws IOException { if (suspended) return; writeBytes(b, off, len); } private void writeBytes(byte b[], int off, int len) throws IOException { if (closed) return; bb.append(b, off, len); bytesWritten += len; // if called from within flush(), then immediately flush // remaining bytes if (doFlush) { bb.flushBuffer(); } } public void writeByte(int b) throws IOException { if (suspended) return; bb.append((byte) b); bytesWritten++; } // ------------------------------------------------- Chars Handling Methods public void write(int c) throws IOException { if (suspended) return; conv.convert((char) c); conv.flushBuffer(); charsWritten++; } public void write(char c[]) throws IOException { if (suspended) return; write(c, 0, c.length); } public void write(char c[], int off, int len) throws IOException { if (suspended) return; conv.convert(c, off, len); conv.flushBuffer(); charsWritten += len; } /** * Append a string to the buffer */ public void write(String s, int off, int len) throws IOException { if (suspended) return; charsWritten += len; if (s == null) s = "null"; conv.convert(s, off, len); conv.flushBuffer(); } public void write(String s) throws IOException { if (suspended) return; if (s == null) s = "null"; conv.convert(s); conv.flushBuffer(); } public void setEncoding(String s) { enc = s; } public void checkConverter() throws IOException { if (!gotEnc) setConverter(); } protected void setConverter() throws IOException { if (coyoteResponse != null) enc = coyoteResponse.getCharacterEncoding(); gotEnc = true; if (enc == null) enc = DEFAULT_ENCODING; conv = (C2BConverter) encoders.get(enc); if (conv == null) { if (Globals.IS_SECURITY_ENABLED){ try{ conv = (C2BConverter)AccessController.doPrivileged( new PrivilegedExceptionAction(){ public Object run() throws IOException{ return new C2BConverter(bb, enc); } } ); }catch(PrivilegedActionException ex){ Exception e = ex.getException(); if (e instanceof IOException) throw (IOException)e; } } else { conv = new C2BConverter(bb, enc); } encoders.put(enc, conv); } } // -------------------- BufferedOutputStream compatibility /** * Real write - this buffer will be sent to the client */ public void flushBytes() throws IOException { bb.flushBuffer(); } public int getBytesWritten() { if (bytesWritten < Integer.MAX_VALUE) { return (int) bytesWritten; } return -1; } public int getCharsWritten() { if (charsWritten < Integer.MAX_VALUE) { return (int) charsWritten; } return -1; } public int getContentWritten() { long size = bytesWritten + charsWritten ; if (size < Integer.MAX_VALUE) { return (int) size; } return -1; } public long getContentWrittenLong() { return bytesWritten + charsWritten; } /** * True if this buffer hasn't been used ( since recycle() ) - * i.e. no chars or bytes have been added to the buffer. */ public boolean isNew() { return (bytesWritten == 0) && (charsWritten == 0); } public void setBufferSize(int size) { if (size > bb.getLimit()) {// ?????? bb.setLimit(size); } } public void reset() { bb.recycle(); bytesWritten = 0; charsWritten = 0; gotEnc = false; enc = null; initial = true; } public int getBufferSize() { return bb.getLimit(); } }