| /* |
| * 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.HashMap; |
| |
| import javax.servlet.WriteListener; |
| import javax.servlet.http.HttpServletResponse; |
| |
| 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; |
| import org.apache.tomcat.util.buf.CharChunk; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * 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 responsibility). |
| * |
| * @author Costin Manolache |
| * @author Remy Maucherat |
| */ |
| public class OutputBuffer extends Writer |
| implements ByteChunk.ByteOutputChannel, CharChunk.CharOutputChannel { |
| |
| private static final StringManager sm = |
| StringManager.getManager(Constants.Package); |
| |
| // -------------------------------------------------------------- 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 final ByteChunk bb; |
| |
| |
| /** |
| * The chunk buffer. |
| */ |
| private final CharChunk cb; |
| |
| |
| /** |
| * 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 final ByteChunk outputChunk = new ByteChunk(); |
| |
| |
| /** |
| * Char chunk used to output chars. |
| */ |
| private CharChunk outputCharChunk = new CharChunk(); |
| |
| |
| /** |
| * Encoding to use. |
| */ |
| private String enc; |
| |
| |
| /** |
| * Encoder is set. |
| */ |
| private boolean gotEnc = false; |
| |
| |
| /** |
| * List of encoders. |
| */ |
| protected final HashMap<String, C2BConverter> encoders = new HashMap<>(); |
| |
| |
| /** |
| * 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); |
| cb = new CharChunk(size); |
| cb.setLimit(size); |
| cb.setOptimizedWrite(false); |
| cb.setCharOutputChannel(this); |
| |
| } |
| |
| |
| // ------------------------------------------------------------- Properties |
| |
| |
| /** |
| * Associated Coyote response. |
| * |
| * @param coyoteResponse Associated Coyote response |
| */ |
| public void setResponse(Response coyoteResponse) { |
| this.coyoteResponse = 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(); |
| cb.recycle(); |
| outputCharChunk.setChars(null, 0, 0); |
| closed = false; |
| suspended = false; |
| doFlush = 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 |
| */ |
| @Override |
| public void close() |
| throws IOException { |
| |
| if (closed) { |
| return; |
| } |
| if (suspended) { |
| return; |
| } |
| |
| // If there are chars, flush all of them to the byte buffer now as bytes are used to |
| // calculate the content-length (if everything fits into the byte buffer, of course). |
| if (cb.getLength() > 0) { |
| cb.flushBuffer(); |
| } |
| |
| if ((!coyoteResponse.isCommitted()) |
| && (coyoteResponse.getContentLengthLong() == -1)) { |
| // If this didn't cause a commit of the response, the final content |
| // length can be calculated |
| if (!coyoteResponse.isCommitted()) { |
| coyoteResponse.setContentLength(bb.getLength()); |
| } |
| } |
| |
| if (coyoteResponse.getStatus() == |
| HttpServletResponse.SC_SWITCHING_PROTOCOLS) { |
| doFlush(true); |
| } else { |
| 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.action(ActionCode.CLOSE, null); |
| } |
| |
| |
| /** |
| * Flush bytes or chars contained in the buffer. |
| * |
| * @throws IOException An underlying IOException occurred |
| */ |
| @Override |
| 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 (cb.getLength() > 0) { |
| cb.flushBuffer(); |
| } |
| if (bb.getLength() > 0) { |
| bb.flushBuffer(); |
| } |
| } finally { |
| doFlush = false; |
| } |
| |
| if (realFlush) { |
| coyoteResponse.action(ActionCode.CLIENT_FLUSH, null); |
| // 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 |
| */ |
| @Override |
| 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 |
| |
| |
| /** |
| * Convert the chars to bytes, then send the data to the client. |
| * |
| * @param buf Char buffer to be written to the response |
| * @param off Offset |
| * @param len Length |
| * |
| * @throws IOException An underlying IOException occurred |
| */ |
| @Override |
| public void realWriteChars(char buf[], int off, int len) |
| throws IOException { |
| |
| outputCharChunk.setChars(buf, off, len); |
| while (outputCharChunk.getLength() > 0) { |
| conv.convert(outputCharChunk, bb); |
| if (bb.getLength() == 0) { |
| // Break out of the loop if more chars are needed to produce any output |
| break; |
| } |
| if (outputCharChunk.getLength() > 0) { |
| if (bb.getBuffer().length == bb.getEnd() && bb.getLength() < bb.getLimit()) { |
| // Need to expand output buffer |
| bb.makeSpace(outputCharChunk.getLength()); |
| } else { |
| bb.flushBuffer(); |
| } |
| } |
| } |
| |
| } |
| |
| @Override |
| public void write(int c) |
| throws IOException { |
| |
| if (suspended) { |
| return; |
| } |
| |
| cb.append((char) c); |
| charsWritten++; |
| |
| } |
| |
| |
| @Override |
| public void write(char c[]) |
| throws IOException { |
| |
| if (suspended) { |
| return; |
| } |
| |
| write(c, 0, c.length); |
| |
| } |
| |
| |
| @Override |
| public void write(char c[], int off, int len) |
| throws IOException { |
| |
| if (suspended) { |
| return; |
| } |
| |
| cb.append(c, off, len); |
| charsWritten += len; |
| |
| } |
| |
| |
| /** |
| * Append a string to the buffer |
| */ |
| @Override |
| public void write(String s, int off, int len) |
| throws IOException { |
| |
| if (suspended) { |
| return; |
| } |
| |
| if (s == null) { |
| throw new NullPointerException(sm.getString("outputBuffer.writeNull")); |
| } |
| cb.append(s, off, len); |
| charsWritten += len; |
| } |
| |
| |
| @Override |
| public void write(String s) |
| throws IOException { |
| |
| if (suspended) { |
| return; |
| } |
| |
| if (s == null) { |
| s = "null"; |
| } |
| cb.append(s); |
| charsWritten += s.length(); |
| } |
| |
| |
| 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 = encoders.get(enc); |
| if (conv == null) { |
| if (Globals.IS_SECURITY_ENABLED){ |
| try{ |
| conv = AccessController.doPrivileged( |
| new PrivilegedExceptionAction<C2BConverter>(){ |
| |
| @Override |
| public C2BConverter run() throws IOException{ |
| return new C2BConverter(enc); |
| } |
| |
| } |
| ); |
| }catch(PrivilegedActionException ex){ |
| Exception e = ex.getException(); |
| if (e instanceof IOException) { |
| throw (IOException)e; |
| } |
| } |
| } else { |
| conv = new C2BConverter(enc); |
| } |
| |
| encoders.put(enc, conv); |
| |
| } |
| } |
| |
| |
| // -------------------- BufferedOutputStream compatibility |
| |
| |
| public long getContentWritten() { |
| return bytesWritten + charsWritten; |
| } |
| |
| /** |
| * Has this buffer been used at all? |
| * |
| * @return true if no chars or bytes have been added to the buffer since the |
| * last call to {@link #recycle()} |
| */ |
| public boolean isNew() { |
| return (bytesWritten == 0) && (charsWritten == 0); |
| } |
| |
| |
| public void setBufferSize(int size) { |
| if (size > bb.getLimit()) {// ?????? |
| bb.setLimit(size); |
| } |
| } |
| |
| |
| public void reset() { |
| reset(false); |
| } |
| |
| public void reset(boolean resetWriterStreamFlags) { |
| bb.recycle(); |
| cb.recycle(); |
| bytesWritten = 0; |
| charsWritten = 0; |
| if (resetWriterStreamFlags) { |
| gotEnc = false; |
| enc = null; |
| } |
| initial = true; |
| } |
| |
| |
| public int getBufferSize() { |
| return bb.getLimit(); |
| } |
| |
| |
| /* |
| * All the non-blocking write state information is held in the Response so |
| * it is visible / accessible to all the code that needs it. |
| */ |
| |
| public boolean isReady() { |
| return coyoteResponse.isReady(); |
| } |
| |
| |
| public void setWriteListener(WriteListener listener) { |
| coyoteResponse.setWriteListener(listener); |
| } |
| |
| |
| public boolean isBlocking() { |
| return coyoteResponse.getWriteListener() == null; |
| } |
| |
| public void checkRegisterForWrite() { |
| coyoteResponse.checkRegisterForWrite(true); |
| } |
| } |