//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.httpmonitor.proxy;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.proxy.ProxyServlet;
import org.eclipse.jetty.util.Fields;

import de.ugoe.cs.autoquest.httpmonitor.exchange.Status;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * the servlet deployed in the web server of the proxy that proxies all incoming messages and
 * forwards a copy of recorded exchanges to the exchange listener manager. It is based on the
 * proxy servlet provided by jetty. It extends the servlet and implements all hooks required to 
 * get access to the exchanged requests and responses. Each hook forwards the tracked data to the
 * exchange listener. On exchange completion, the exchange listener ensures a logging of the
 * recorded exchange.
 * </p>
 * <p>
 * The implementation also overrides the default implementation of the jetty HTTP client
 * and the jetty HTTP request to fix a bug in forwarding queries.
 * </p>
 * 
 * @author Patrick Harms
 */
class HttpMonitoringProxyServlet extends ProxyServlet {

    /**  */
    private static final long serialVersionUID = 1L;

    /**
     * the proxied server
     */
    private String proxiedServer;
    
    /**
     * the port of the proxied server
     */
    private int proxiedPort;

    /**
     * the exchange listener to handle the different events happening when an exchange is proxied
     */
    private transient ExchangeListenerManager exchangeListenerManager;
    
    /**
     * <p>
     * initializes the servlet with the proxied server and the exchange listener
     * </p>
     *
     * @param proxiedServer           the proxied server
     * @param proxiedPort             the port of the proxied server
     * @param exchangeListenerManager the exchange listener to handle the different events
     *                                happening when an exchange is proxied
     */
    HttpMonitoringProxyServlet(String                  proxiedServer,
                               int                     proxiedPort,
                               ExchangeListenerManager exchangeListenerManager)
    {
        this.proxiedServer = proxiedServer;
        this.proxiedPort = proxiedPort;
        this.exchangeListenerManager = exchangeListenerManager;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jetty.proxy.ProxyServlet#rewriteURI(HttpServletRequest)
     */
    @Override
    protected URI rewriteURI(HttpServletRequest request) {
        try {
            return new URI(request.getScheme(), null, proxiedServer, proxiedPort,
                           request.getPathInfo(), request.getQueryString(), null);
        }
        catch (URISyntaxException e) {
            Console.printerrln("could not rewrite URI: " + e);
            Console.logException(e);
            return null;
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.jetty.proxy.ProxyServlet#newHttpClient()
     */
    @Override
    protected HttpClient newHttpClient() {
        return new BugfixedHttpClient();
    }

    /* (non-Javadoc)
     * @see org.eclipse.jetty.proxy.ProxyServlet#customizeProxyRequest(Request, HttpServletRequest)
     */
    @Override
    protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request) {
        super.customizeProxyRequest(proxyRequest, request);
        
        exchangeListenerManager.onRequest(request);
        proxyRequest.content(new DubbingContentProvider(request, proxyRequest.getContent()));
    }

    /* (non-Javadoc)
     * @see ProxyServlet#onResponseContent(HttpServletRequest, HttpServletResponse, Response, byte[], int, int)
     */
    @Override
    protected void onResponseContent(HttpServletRequest  request,
                                     HttpServletResponse response,
                                     Response            proxyResponse,
                                     byte[]              buffer,
                                     int                 offset,
                                     int                 length)
        throws IOException
    {
        super.onResponseContent(request, response, proxyResponse, buffer, offset, length);
        
        exchangeListenerManager.onResponseContent(request, ByteBuffer.wrap(buffer, offset, length));
    }

    /* (non-Javadoc)
     * @see ProxyServlet#onResponseSuccess(HttpServletRequest, HttpServletResponse, Response)
     */
    @Override
    protected void onResponseSuccess(HttpServletRequest  request,
                                     HttpServletResponse response,
                                     Response            proxyResponse)
    {
        exchangeListenerManager.onResponse(request, response);
        exchangeListenerManager.onFinish(request, Status.SUCCESS);
        
        super.onResponseSuccess(request, response, proxyResponse);
    }

    /* (non-Javadoc)
     * @see ProxyServlet#onResponseFailure(HttpServletRequest, HttpServletResponse, Response, Throwable)
     */
    @Override
    protected void onResponseFailure(HttpServletRequest  request,
                                     HttpServletResponse response,
                                     Response            proxyResponse,
                                     Throwable           failure)
    {
        exchangeListenerManager.onResponse(request, response);
        exchangeListenerManager.onFinish(request, Status.FAILURE);
        
        super.onResponseFailure(request, response, proxyResponse, failure);
    }

    /**
     * This content provided is required to copy the content of a proxied request. It uses
     * delegation to wrap the original content provided but to also copy the data and forward
     * it to the exchange listener manager.
     */
    private class DubbingContentProvider implements ContentProvider {

        /**
         * the dubbed request
         */
        private HttpServletRequest request;
        
        /**
         * the original content provider of which the data is copied 
         */
        private ContentProvider delegate;

        /**
         * initializes this content provider with the copied request and the delegate.
         */
        public DubbingContentProvider(HttpServletRequest request, ContentProvider delegate) {
            this.request = request;
            this.delegate = delegate;
        }

        /* (non-Javadoc)
         * @see java.lang.Iterable#iterator()
         */
        @Override
        public Iterator<ByteBuffer> iterator() {
            return new DubbingByteBufferIterator(request, delegate.iterator());
        }

        /* (non-Javadoc)
         * @see org.eclipse.jetty.client.api.ContentProvider#getLength()
         */
        @Override
        public long getLength() {
            return delegate.getLength();
        }

    }

    /**
     * This iterator is used to implement the {@link DubbingContentProvider}. It uses delegation
     * for wrapping the original iterator and forwards all copied data to the exchange listener
     * manager. Furthermore, it combines several buffers into one. This seems to be required if
     * SOAP messages are proxied.
     */
    public class DubbingByteBufferIterator implements Iterator<ByteBuffer> {

        /**
         * the dubbed request
         */
        private HttpServletRequest request;
        
        /**
         * the original iterator of which the data is copied 
         */
        private Iterator<ByteBuffer> delegate;
        
        /**
         * initializes this iterator with the copied request and the delegate.
         */
        public DubbingByteBufferIterator(HttpServletRequest request, Iterator<ByteBuffer> delegate) {
            this.request = request;
            this.delegate = delegate;
        }

        /* (non-Javadoc)
         * @see java.util.Iterator#hasNext()
         */
        @Override
        public boolean hasNext() {
            return delegate.hasNext();
        }

        /* (non-Javadoc)
         * @see java.util.Iterator#next()
         */
        @Override
        public ByteBuffer next() {
            List<ByteBuffer> buffers = new LinkedList<ByteBuffer>();
            
            int size = 0;
            while (delegate.hasNext()) {
                ByteBuffer next = delegate.next();
                exchangeListenerManager.onRequestContent(request, next.duplicate());

                ByteBuffer copy = ByteBuffer.allocate(next.limit());
                copy.put(next);
                copy.position(0);
                buffers.add(copy);
                
                size += next.limit();
            }
            
            ByteBuffer buffer = ByteBuffer.allocate(size);
            
            for (ByteBuffer orig : buffers) {
                buffer.put(orig);
            }
            
            buffer.position(0);
            
            return buffer;
        }

        /* (non-Javadoc)
         * @see java.util.Iterator#remove()
         */
        @Override
        public void remove() {
            while (delegate.hasNext()) {
                delegate.remove();
            }
        }

    }

    /**
     * <p>
     * bugfix implementation of the jetty HTTP client to be able to use only bugfixed HTTP requests
     * </p>
     * 
     * @author Patrick Harms
     */
    private static class BugfixedHttpClient extends HttpClient {

        /* (non-Javadoc)
         * @see org.eclipse.jetty.client.HttpClient#newRequest(java.net.URI)
         */
        @Override
        public Request newRequest(URI uri) {
            return new BugfixedHttpRequest(this, uri);
        }

    }

    /**
     * <p>
     * bugfix implementation of the jetty HTTP request. This ensures that query string
     * representations are correctly forwarded in the case that a query parameter does not have
     * a value. 
     * </p>
     * 
     * @author Patrick Harms
     */
    private static class BugfixedHttpRequest extends HttpRequest {

        /**
         * <p>
         * Constructor to override parent constructor
         * </p> 
         */
        BugfixedHttpRequest(HttpClient client, URI uri) {
            super(client, uri);
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.eclipse.jetty.client.HttpRequest#getQuery()
         */
        @Override
        public String getQuery() {
            return buildQuery();
        }

        /**
         * <p>
         * this corrects the bug implemented in the parent class on creating the query string
         * </p>
         * 
         * @return the correct query string
         */
        private String buildQuery() {
            StringBuilder result = new StringBuilder();
            for (Iterator<Fields.Field> iterator = super.getParams().iterator(); iterator.hasNext();)
            {
                Fields.Field field = iterator.next();
                String[] values = field.values();
                for (int i = 0; i < values.length; ++i) {
                    if (i > 0) {
                        result.append("&");
                    }

                    result.append(field.name());

                    if ((values[i] != null) && (!"".equals(values[i].trim()))) {
                        result.append("=");
                        result.append(urlEncode(values[i]));
                    }
                }
                if (iterator.hasNext()) {
                    result.append("&");
                }
            }
            return result.toString();
        }

        /**
         * <p>
         * convenience method copied from parent class, as it is private there.
         * </p>
         * 
         * @see HttpRequest#urlEncode(String)
         */
        private String urlEncode(String value) {
            String encoding = "UTF-8";
            try {
                return URLEncoder.encode(value, encoding);
            }
            catch (UnsupportedEncodingException e) {
                throw new UnsupportedCharsetException(encoding);
            }
        }
    }
}
