/*
 * Copyright 1999,2004 The Apache Software Foundation.
 *
 * Licensed 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.jboss.remoting.transport.coyote;


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 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.jboss.remoting.util.SecurityUtility;


/**
 * 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, CharChunk.CharOutputChannel
{

   // -------------------------------------------------------------- Constants


   public static final String DEFAULT_ENCODING =
         org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING;
   public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;


   // The buffer can be used for byte[] and char[] writing
   // ( this is needed to support ServletOutputStream and for
   // efficient implementations of templating systems )
   public final int INITIAL_STATE = 0;
   public final int CHAR_STATE = 1;
   public final int BYTE_STATE = 2;

   // ----------------------------------------------------- Instance Variables


   /**
    * The byte buffer.
    */
   private ByteChunk bb;


   /**
    * The chunk buffer.
    */
   private CharChunk cb;


   /**
    * State of the output buffer.
    */
   private int state = 0;


   /**
    * Number of bytes written.
    */
   private int bytesWritten = 0;


   /**
    * Number of chars written.
    */
   private int 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 HashMap 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);
      // Allow buffer to grow, to avoid using chunking
      // FIXME: Allowing chunking could be a better idea
      bb.setLimit(-1);
      bb.setByteOutputChannel(this);
      cb = new CharChunk(size);
      cb.setCharOutputChannel(this);
      cb.setLimit(size);

   }

   // ------------------------------------------------------------- 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;
   }

   // --------------------------------------------------------- Public Methods


   /**
    * Recycle the output buffer.
    */
   public void recycle()
   {

      state = INITIAL_STATE;
      bytesWritten = 0;
      charsWritten = 0;

      cb.recycle();
      bb.recycle();
      closed = false;
      suspended = false;

      if(conv != null)
      {
         conv.recycle();
      }

      gotEnc = false;
      enc = null;

   }


   /**
    * 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))
      {
         // Flushing the char buffer
         if(state == CHAR_STATE)
         {
            cb.flushBuffer();
            state = BYTE_STATE;
         }
         // If this didn't cause a commit of the response, the final content
         // length can be calculated
         if(!coyoteResponse.isCommitted())
         {
            coyoteResponse.setContentLength(bb.getLength());
         }
      }

      doFlush(false);
      closed = true;

      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;
      }

      doFlush = true;
      if(state == CHAR_STATE)
      {
         cb.flushBuffer();
         bb.flushBuffer();
         state = BYTE_STATE;
      }
      else if(state == BYTE_STATE)
      {
         bb.flushBuffer();
      }
      else if(state == INITIAL_STATE)
      {
         // If the buffers are empty, commit the response header
         coyoteResponse.sendHeaders();
      }
      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;
      }

      if(state == CHAR_STATE)
      {
         cb.flushBuffer();
      }
      state = BYTE_STATE;
      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;
      }

      if(state == CHAR_STATE)
      {
         cb.flushBuffer();
      }
      state = BYTE_STATE;

      bb.append((byte) b);
      bytesWritten++;

   }

   // ------------------------------------------------- Chars Handling Methods


   public void write(int c)
         throws IOException
   {

      if(suspended)
      {
         return;
      }

      state = CHAR_STATE;

      cb.append((char) c);
      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;
      }

      state = CHAR_STATE;

      cb.append(c, off, len);
      charsWritten += len;

   }


   public void write(StringBuffer sb)
         throws IOException
   {

      if(suspended)
      {
         return;
      }

      state = CHAR_STATE;

      int len = sb.length();
      charsWritten += len;
      cb.append(sb);

   }


   /**
    * Append a string to the buffer
    */
   public void write(String s, int off, int len)
         throws IOException
   {

      if(suspended)
      {
         return;
      }

      state = CHAR_STATE;

      charsWritten += len;
      if(s == null)
      {
         s = "null";
      }
      cb.append(s, off, len);

   }


   public void write(String s)
         throws IOException
   {

      if(suspended)
      {
         return;
      }

      state = CHAR_STATE;
      if(s == null)
      {
         s = "null";
      }
      write(s, 0, s.length());

   }


   public void flushChars()
         throws IOException
   {

      cb.flushBuffer();
      state = BYTE_STATE;

   }


   public boolean flushCharsNeeded()
   {
      return state == CHAR_STATE;
   }


   public void setEncoding(String s)
   {
      enc = s;
   }


   public void realWriteChars(char c[], int off, int len)
         throws IOException
   {

      if(!gotEnc)
      {
         setConverter();
      }

      // The following has been updated to conform to jbossweb 2.1.
      cb.setChars(c, off, len);
      while (cb.getLength() > 0) { 
          conv.convert(cb, bb);
          if (cb.getLength() > 0) {
              bb.flushBuffer();
          }
      }

   }


   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);
      
      // The following has been updated to conform to jbossweb 2.1.
      if(conv == null)
      {

         if(!SecurityUtility.skipAccessControl())
         {
            try
            {
               conv = (C2BConverter) AccessController.doPrivileged(
                     new PrivilegedExceptionAction()
                     {

                        public Object 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


   /**
    * Real write - this buffer will be sent to the client
    */
   public void flushBytes()
         throws IOException
   {

      bb.flushBuffer();

   }


   public int getBytesWritten()
   {
      return bytesWritten;
   }


   public int getCharsWritten()
   {
      return charsWritten;
   }


   public int getContentWritten()
   {
      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()
   {

      //count=0;
      bb.recycle();
      bytesWritten = 0;
      cb.recycle();
      charsWritten = 0;
      gotEnc = false;
      enc = null;
      state = INITIAL_STATE;
   }


   public int getBufferSize()
   {
      return bb.getLimit();
   }


}
