/*
 * Copyright 2003 Jayson Falkner (jayson@jspinsider.com)
 * This code is from "Servlets and JavaServer pages; the J2EE Web Tier",
 * http://www.jspbook.com. You may freely use the code both commercially
 * and non-commercially. If you like the code, please pick up a copy of
 * the book and help support the authors, development of more free code,
 * and the JSP/Servlet/J2EE community.
 */
package ****;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.log4j.Logger;

/**
 * Dieser Filter ermoeglicht die Kompression von Inhalten.
 * 
 * Wie immer sind im großen und ganzen drei Klassen noetig:</br>
 * A) die Filterklasse (implements Filter)</br>
 * B) einen ResponseWrapper (extends HttpServletResponseWrapper und dekoriert die 
 * Originalresponse)</br>
 * C) ein OutputStream (extends ServletOutputStream und dekoriert den original
 * OutputStream</br>
 * Ich habe diese Klassen als statische innere Klassen designed, weil ich im Moment den
 * Filter als den einzigen öffentlichen Part ansehe.
 * <p>
 * Die Aktivierung des Filters erfolgt in der web.xml der
 * betreffenden Webanwendung und sollte auf Textinhalte eingeschraenkt
 * werden. Die Verwendung des Filters bedeutet ein Kompromiss zwischen
 * der zusaetzlichen CPU Last aufgrund der ZIP Kompression einerseits
 * und der schnelleren Uebertragung andererseits. Eine Messung des
 * Effekts in einer produktiven Umgebung ist dahern unerlaesslich.
 * <p>
 * Beispiel für die web.xml:
 * 
 *  <filter>
 *    <filter-name>Compress</filter-name>
 *    <filter-class>de.volkswagen.kpm.webapp.compressor.GZIPFilter</filter-class>
 *  </filter>
 *  <filter-mapping>
 *    <filter-name>Compress</filter-name>
 *    <url-pattern>*.html</url-pattern>
 *  </filter-mapping>
 */
public class GZIPFilter implements Filter {

  static final Logger LOGGER = Logger.getLogger(GZIPFilter.class);

  public void doFilter(ServletRequest req, ServletResponse res,
    FilterChain chain) throws IOException, ServletException {

    // Filter lieber nur fuer HTTP Verkehr verwenden!
    if (req instanceof HttpServletRequest) {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;
      // Kann denn der Client (Browser) uebrhaupt gzip-Formate?
      // (alle modernen Browser seit Mosaic machen das richtig)
      String ae = request.getHeader("accept-encoding");
      if (ae != null && ae.indexOf("gzip") != -1) {

        // LOGGER.info("GZIP runs for " + request.getRequestURI());
        // der Responsestrom wird in einen Wrapper gepackt und dieser dann benutzt
        GZIPResponseWrapper wrappedResponse = new GZIPResponseWrapper(response);
        chain.doFilter(req, wrappedResponse);
        wrappedResponse.finishResponse();
        return;
      }
      // Filterkette fortsetzen 
      chain.doFilter(req, res);
    }
  }

  public void init(FilterConfig filterConfig) {
    // noop
  }

  public void destroy() {
    // noop
  }

  /**
   * Der Wrapper fuer den HttpServletResponse.
   * 
   * Die Implementierungen sind soweit klar, die Musike spielt in
   * createOutputStream - hier wird auf den entscheidenden
   * GZIPResponseStream verwiesen. 
   */
  private static class GZIPResponseWrapper extends HttpServletResponseWrapper {
    protected HttpServletResponse origResponse = null;
    protected ServletOutputStream stream = null;
    protected PrintWriter writer = null;

    public GZIPResponseWrapper(HttpServletResponse response) {
      super(response);
      origResponse = response;
    }

    public ServletOutputStream createOutputStream() throws IOException {
      return (new GZIPResponseStream(origResponse));
    }

    public void finishResponse() {
      try {
        if (writer != null) {
          writer.close();
        } else {
          if (stream != null) {
            stream.close();
          }
        }
      } catch (IOException e) {
      }
    }

    public void flushBuffer() throws IOException {
      stream.flush();
    }

    public ServletOutputStream getOutputStream() throws IOException {
      if (writer != null) {
        throw new IllegalStateException("getWriter() has already been called!");
      }

      if (stream == null)
        stream = createOutputStream();
      return (stream);
    }

    public PrintWriter getWriter() throws IOException {
      if (writer != null) {
        return (writer);
      }

      if (stream != null) {
        throw new IllegalStateException(
          "getOutputStream() has already been called!");
      }

      stream = createOutputStream();
      writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8"));
      return (writer);
    }

    public void setContentLength(int length) {
    }
  }
  
  /**
   * Die Filterimplementierung eines ServletOutputStream.
   * 
   * Die Musike spielt im Konstruktor und in write.
   */
  private static class GZIPResponseStream extends ServletOutputStream {
    protected ByteArrayOutputStream baos = null;
    protected GZIPOutputStream gzipstream = null;
    protected boolean closed = false;
    protected HttpServletResponse response = null;
    protected ServletOutputStream output = null;

    public GZIPResponseStream(HttpServletResponse response) throws IOException {
      super();
      closed = false;
      this.response = response;
      this.output = response.getOutputStream();
      baos = new ByteArrayOutputStream();
      gzipstream = new GZIPOutputStream(baos);
    }

    public void close() throws IOException {
      if (closed) {
        throw new IOException("This output stream has already been closed");
      }
      gzipstream.finish();

      byte[] bytes = baos.toByteArray();

      response.addHeader("Content-Length", Integer.toString(bytes.length)); 
      response.addHeader("Content-Encoding", "gzip");
      
      output.write(bytes);
      output.flush();
      output.close();
      closed = true;
    }

    public void flush() throws IOException {
      if (closed) {
        throw new IOException("Cannot flush a closed output stream");
      }
      gzipstream.flush();
    }

    public void write(int b) throws IOException {
      if (closed) {
        throw new IOException("Cannot write to a closed output stream");
      }
      gzipstream.write((byte)b);
    }

    public void write(byte b[]) throws IOException {
      write(b, 0, b.length);
    }

    public void write(byte b[], int off, int len) throws IOException {
      if (closed) {
        throw new IOException("Cannot write to a closed output stream");
      }
      gzipstream.write(b, off, len);
    }

    public boolean closed() {
      return (this.closed);
    }
    
    public void reset() {
      //noop
    }
  }
}
