/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: FilteredOutputStream.java 20987 2011-03-14 12:10:10Z benoitf $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.tomcat7.versioning;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.apache.catalina.connector.OutputBuffer;
import org.apache.catalina.connector.Response;
import org.ow2.jonas.versioning.VersioningService;

/**
 * Output stream with content filtering.
 *
 * @author tokmen-sa
 */
public class FilteredOutputStream extends OutputBuffer {

    /**
     * List of endocings we support for byte array conversion.<br/>
     * <br/>
     * <b>IMPORTANT NOTE</b>: adding multi-byte encodings here (for example,
     * UTF-8) is not supported yet.
     */
    private static List<String> SUPPORTED_BYTE_ARRAY_ENCODINGS = Arrays.asList(new String[] {
        "US-ASCII",
        "ISO-8859-1"
    });

    private Response response;

    private int remainingChars = 0;

    public FilteredOutputStream(Response response) {
        super();
        this.response = response;
    }

    public FilteredOutputStream(Response response, int size) {
        super(size);
        this.response = response;
    }

    @Override
    public void writeByte(int b) throws IOException {
        String encoding = response.getCharacterEncoding();

        if (encoding == null || !FilteredOutputStream.SUPPORTED_BYTE_ARRAY_ENCODINGS.contains(encoding)) {
            super.writeByte(b);
        } else {
            super.checkConverter();
            this.doWrite(new String(new byte[]{(byte) b}, 0, 1, encoding), 0, 1);
        }
    }

    @Override
    public void write(byte buf[], int off, int cnt) throws IOException {
        String encoding = response.getCharacterEncoding();

        if (encoding == null || !FilteredOutputStream.SUPPORTED_BYTE_ARRAY_ENCODINGS.contains(encoding)) {
            super.write(buf, off, cnt);
        } else {
            super.checkConverter();
            this.doWrite(new String(buf, off, cnt, encoding), 0, cnt);
        }
    }

    @Override
    public void write(String s, int off, int cnt) throws IOException {
        if (s == null) {
            super.write(s, off, cnt);
        } else {
            this.doWrite(s, off, cnt);
        }
    }

    @Override
    public void write(char buf[], int off, int cnt) throws IOException {
        if (buf == null) {
            super.write(buf, off, cnt);
        } else {
            this.doWrite(new String(buf, off, cnt), 0, cnt);
        }
    }

    /**
     * Replaces all versioned paths with the user path in the response output
     * stream.
     */
    protected void doWrite(String s, int off, int cnt) throws IOException {
        VersionedPathBean vpb = this.findVersionedPathBean(response.getContentType());

        // Now rewriting, for example:
        //
        //  <img src="/path-version-1.0.0/image.gif">
        // into
        //  <img src="/path/image.gif">
        if (vpb != null) {
            if (this.remainingChars != 0) {
                final int remainingCompare = vpb.getVersionedPath().length() - this.remainingChars;
                final String remainingVersionedPath = vpb.getVersionedPath().substring(remainingCompare);

                final int compareLength = Math.min(remainingVersionedPath.length(), cnt);
                final String expected = remainingVersionedPath.substring(0, compareLength);
                final String actual = s.substring(off, off + compareLength);
                if (expected.equals(actual)) {
                    if (compareLength == remainingVersionedPath.length()) {
                        // Matched the versioned path
                        // No content to write before

                        // Write the user path
                        if (!"/".equals(vpb.getUserPath())) {
                            super.write(vpb.getUserPath());
                            this.remainingChars = 0;
                        }

                        // Prepare to write the rest
                        off += compareLength;
                        cnt -= compareLength;
                    } else {
                        // Partial match
                        this.remainingChars -= compareLength;

                        if (this.remainingChars > vpb.getVersionedPath().length()) {
                            throw new IllegalStateException("Remaining characters has exceeded versioned path length!");
                        }
                        return;
                    }
                } else {
                    super.write(vpb.getVersionedPath().substring(0, remainingCompare));
                    this.remainingChars = 0;
                }
            }

            final int originalOffset = off;
            final int originalCount = cnt;
            final int originalLength = originalOffset + originalCount;

            for (int i = originalOffset; i < originalLength; i++) {
                if (vpb.getVersionedPath().charAt(0) == s.charAt(i)) {
                    final int compareLength = Math.min(vpb.getVersionedPath().length(), originalLength - i);
                    final String expected = vpb.getVersionedPath().substring(0, compareLength);
                    final String actual = s.substring(i, i + compareLength);
                    if (expected.equals(actual)) {
                        // Matched all of part of the the versioned path
                        // Write out the beginning: <img src="
                        final String upToHere = s.substring(off, i);
                        super.write(upToHere);

                        if (compareLength == vpb.getVersionedPath().length()) {
                            // Write the user path
                            if (!"/".equals(vpb.getUserPath())) {
                                super.write(vpb.getUserPath());
                            }

                            // Prepare to write the rest
                            i += compareLength;
                            off = i;
                            cnt = originalLength - off;
                        } else {
                            // Partial match
                            this.remainingChars = vpb.getVersionedPath().length() - compareLength;

                            if (this.remainingChars > vpb.getVersionedPath().length()) {
                                throw new IllegalStateException("Remaining characters has exceeded versioned path length!");
                            }
                            return;
                        }
                    }
                }
            }
        }

        // Write out the end: /image.gif">
        if (s.length() < off + cnt) {
            throw new IllegalStateException(s);
        }
        super.write(s, off, cnt);
    }

    private VersionedPathBean findVersionedPathBean(String contentType) {
        // Get path information and whether we should overwrite the stream.
        if (contentType != null && ContextFinder.getParent() != null) {
            VersioningService versioning = ContextFinder.getParent().getVersioningService();
            VersionedPathBean vpb = response.getVersionedPathBean();

            // Is this a versioned request?
            if (vpb != null && vpb.getUserPath() != null && vpb.getVersionedPath() != null && versioning != null
                && versioning.isVersioningEnabled()) {

                String[] filteredContentTypes = versioning.getFilteredContentTypesArray();
                if (filteredContentTypes != null && filteredContentTypes.length > 0) {
                    // For this versioned request, is this response type
                    // supposed to be rewritten?
                    for (String filteredContentType : filteredContentTypes) {
                        if (contentType.startsWith(filteredContentType)) {
                            return vpb;
                        }
                    }
                }
            }
        }

        return null;
    }
}
