/* Copyright 2004 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.apache.jetspeed.portlet;

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

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletContext;
import javax.portlet.PortletException;
import javax.portlet.PortletMode;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletSession;
import javax.portlet.PortletURL;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import org.apache.portals.bridges.velocity.GenericVelocityPortlet;
import org.apache.portals.messaging.PortletMessaging;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.jetspeed.rewriter.JetspeedRewriterController;
import org.apache.jetspeed.rewriter.RewriterController;
import org.apache.jetspeed.rewriter.RewriterException;
import org.apache.jetspeed.rewriter.RulesetRewriter;
import org.apache.jetspeed.rewriter.WebContentRewriter;
import org.apache.jetspeed.rewriter.html.SwingParserAdaptor;
import org.apache.jetspeed.rewriter.rules.Ruleset;
import org.apache.jetspeed.rewriter.xml.SaxParserAdaptor;

//standard java stuff
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.MalformedURLException;

import java.io.Reader;

import java.net.URL;
import java.net.URLConnection;
import java.util.StringTokenizer;

/**
 * WebContentPortlet
 * 
 * TODO: Preferences, cache stream instead of URL *
 * 
 * @author <a href="mailto:rogerrutr@apache.org">Roger Ruttimann </a>
 * @version $Id: WebContentPortlet.java,v 1.12 2005/03/01 22:19:54 shinsuke Exp $
 */

public class WebContentPortlet extends GenericVelocityPortlet
{

    /**
     * WebContentPortlet Allows navigation inside the portlet and caches the
     * latest URL
     */

    /**
     * Configuration constants.
     */
    public static final String VIEW_SOURCE_PARAM = "viewSource";

    public static final String EDIT_SOURCE_PARAM = "editSource";

    /**
     * Default WebContent source attribute members.
     */
    private String defaultViewSource;

    private String defaultEditSource;

    /**
     * Action Parameter
     */

    /** WebContent Messages 
     *  TODO: this is a simple implementation
     *  until we introduce a more sophisticated caching algorithm 
     * 
     * */
    public static final String CURRENT_URL = "webcontent.url.current";
    public static final String LAST_URL = "webcontent.url.last";
    public static final String LAST_STATE = "webcontent.last.state";
    public static final String CACHE = "webcontent.cache";

    /** Default encoding */
    public String defaultEncoding = "UTF-8";

    /* SSO settings */
    boolean isSSOEnabled = false;

    /* WebContent rewriter */
    RulesetRewriter rewriter = null;

    RewriterController rewriteController = null;

    
    public WebContentPortlet()
    {
        super();

    }

    /**
     * Initialize portlet configuration.
     */
    public void init(PortletConfig config) throws PortletException
    {
        super.init(config);

        defaultEditSource = config.getInitParameter(EDIT_SOURCE_PARAM);
    }

    /**
     * processAction() Checks action initiated by the WebContent portlet which
     * means that a user has clicked on an URL
     * 
     * @param actionRequest
     * @param actionResponse
     * @throws PortletException
     * @throws IOException
     */
    public void processAction(ActionRequest actionRequest, ActionResponse actionResponse) throws PortletException,
            IOException
    {
        // Check if an action parameter was defined        
        String webContentParameter = actionRequest.getParameter(WebContentRewriter.ACTION_PARAMETER_URL);
        
        if (webContentParameter == null || actionRequest.getPortletMode() == PortletMode.EDIT)
        {
            processPreferencesAction(actionRequest, actionResponse);            
            webContentParameter = actionRequest.getPreferences().getValue("SRC", "http://portals.apache.org");
        }
        

        /*
         * If the webContentParameter is not empty attach the URL to the session
         */
        if (webContentParameter != null && webContentParameter.length() > 0)
        {
            String sessionObj = new String(webContentParameter);
            PortletMessaging.publish(actionRequest, CURRENT_URL, sessionObj);
        }
    }

    /**
     * doView Renders the URL in the following order 1) SESSION_PARAMETER
     * 2)cached version 3) defined for preference SRC
     */
    public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        String viewPage = (String)request.getAttribute(PARAM_VIEW_PAGE);
        if (viewPage != null)
        {
            super.doView(request, response);
            return;
        }
        
        // Find the source URL to execute
        String sourceURL = null;
        String lastURL = null;
        boolean useCache = false;
        
        // Check if the source was defined in the session
        try
        {
            sourceURL = (String)PortletMessaging.receive(request, CURRENT_URL);

            lastURL  = (String)PortletMessaging.receive(request, LAST_URL);
            // TODO: This is just a kludge. Filtering of bad uRL's should be
            // more sophisticated
            if (sourceURL.startsWith("/") || sourceURL.startsWith("..")) sourceURL = null;
        }
        catch (Exception e)
        {
            sourceURL = null;
        }

        // Check if the page was rendered at least once
        if (sourceURL == null && lastURL != null)
        {
            // Use the cache
            sourceURL = lastURL;
            useCache = true;
        }
        
        if (sourceURL == null)
        {
            // Use the URL defined in the preferences
            sourceURL =  request.getPreferences().getValue("SRC", "");
        }

        if (lastURL != null && sourceURL.equals(lastURL))
        {
            useCache = true;
        }
        
        // If all above fails throw an error asking the user to define an URL in
        // edit mode
        if (sourceURL == null)
                throw new PortletException("WebContent source not specified. Go to edit mode and specify an URL.");

        // Initialize the controller if it's not already done
        if (rewriteController == null)
        {
            PortletContext portletApplication = getPortletContext(); 
            String path = portletApplication.getRealPath("/WEB-INF");
            String contextPath = path + "/";
            try
            {
                // Create rewriter adaptor
                rewriteController = getController(contextPath);
            }
            catch (Exception e)
            {
                // Failed to create rewriter controller
                throw new PortletException("WebContentProtlet failed to create rewriter controller. Error:"
                        + e.getMessage());
            }
        }

        String lastState = (String)PortletMessaging.receive(request, LAST_STATE);
        String newState = request.getWindowState().toString();
        if (lastState == null || newState == null || !lastState.equals(newState))
        {
            useCache = false;
        }

        // Set the content type
        response.setContentType("text/html");
        byte[] content;
        byte[] cache = (byte[])request.getPortletSession().getAttribute(CACHE, PortletSession.PORTLET_SCOPE);
        if (useCache && cache != null)
        {
            content = cache;            
        }
        else
        {
            // Draw the content            
            content = doWebContent(request, sourceURL, response);
            request.getPortletSession().setAttribute(CACHE, content, PortletSession.PORTLET_SCOPE);
        }

        // drain the stream to the portlet window
        ByteArrayInputStream bais = new ByteArrayInputStream(content);
        drain(new InputStreamReader(bais, this.defaultEncoding), response.getWriter());
        bais.close();
        
        // Done just save the last URL
        lastURL = sourceURL;
        PortletMessaging.publish(request, LAST_URL, lastURL);
        PortletMessaging.publish(request, LAST_STATE, newState);
    }

    public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        response.setContentType("text/html");
        doPreferencesEdit(request, response);
    }
        
    /*
     * Privaye helpers for generating WebContent
     */
    protected byte[] doWebContent(RenderRequest request, String sourceAttr, RenderResponse response)
            throws PortletException
    {
        // Initialization
        Writer htmlWriter = null;

        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();

        try
        {
            htmlWriter = new OutputStreamWriter(byteOutputStream, this.defaultEncoding);

            // Set the action URL in the rewriter
           PortletURL action = response.createActionURL();
           ((WebContentRewriter) rewriter).setActionURL(action);

            URL baseURL = new URL(sourceAttr);
            String baseurl = baseURL.getProtocol() + "://" + baseURL.getHost();

            rewriter.setBaseUrl(baseurl);
            String source = getURLSource(sourceAttr, request, response);
            // System.out.println("Rewriting SOURCE: " + source);
            rewriter.rewrite(rewriteController.createParserAdaptor("text/html"), getRemoteReader(source), htmlWriter);
            htmlWriter.flush();
            
        }
        catch (UnsupportedEncodingException ueex)
        {
            throw new PortletException("Encoding " + defaultEncoding + " not supported. Error: " + ueex.getMessage());
        }
        catch (RewriterException rwe)
        {
            throw new PortletException("Failed to rewrite HTML page. Error: " + rwe.getMessage());
        }
        catch (Exception e)
        {
            throw new PortletException("Exception while rewritting HTML page. Error: " + e.getMessage());
        }

        // Page has been rewritten
        // TODO: Write it to cache
        return byteOutputStream.toByteArray();
    }

    public String getURLSource(String source, RenderRequest request, RenderResponse response)
    {
        return source;    
    }
    
    /*
     * Get WebContent source preference value
     */
    private String getSourcePreference(RenderRequest request, String name, String defaultValue)
    {
        PortletPreferences prefs = request.getPreferences();
        return ((prefs != null) ? prefs.getValue(name, defaultValue) : defaultValue);
    }

    /*
     * Generate a rewrite controller using the basic rules file
     */
    private RewriterController getController(String contextPath) throws Exception
    {
        Class[] rewriterClasses = new Class[]
        { WebContentRewriter.class, WebContentRewriter.class};
        
        Class[] adaptorClasses = new Class[]
        { SwingParserAdaptor.class, SaxParserAdaptor.class};
        RewriterController rwc = new JetspeedRewriterController(contextPath + "conf/rewriter-rules-mapping.xml", Arrays
                .asList(rewriterClasses), Arrays.asList(adaptorClasses));

        FileReader reader = new FileReader(contextPath + "conf/default-rewriter-rules.xml");

        Ruleset ruleset = rwc.loadRuleset(reader);
        reader.close();
        rewriter = rwc.createRewriter(ruleset);
        return rwc;
    }

    /*
     * getReaderForURL() Streams the page from the uRL into the reader
     */
    protected Reader getReader(String url) throws PortletException
    {
        URL pageUrl = null;
        URLConnection pageConn = null;

        // Open the connection to the page
        try
        {
            pageUrl = new URL(url);
            pageConn = pageUrl.openConnection();

            if (this.isSSOEnabled == true)
            {
                /*
                 * TODO: SSO should provide username & password
                 * 
                 * String username, password; // set HTTP Basic Authetication
                 * header if username and password are set if (username != null &&
                 * password !=null) {
                 * pageConn.setRequestProperty("Authorization", "Basic " +
                 * Base64.encodeAsString(username + ":" + password)); }
                 */
            }
        }
        catch (MalformedURLException urle)
        {
            throw new PortletException("Malformed URL. Error: " + urle.getMessage());
        }
        catch (IOException ioe)
        {
            throw new PortletException("Failed connecting to URL. Error: " + ioe.getMessage());
        }
        catch (Exception e)
        {
            throw new PortletException("Failed connecting to URL. Error: " + e.getMessage());
        }

        long pageExpiration = pageConn.getExpiration();
        String encoding = defaultEncoding;
        String contentType = pageConn.getContentType();
        String tempString = null;
        String noCache = "no-cache";

        if (contentType != null)
        {
            StringTokenizer st = new StringTokenizer(contentType, "; =");
            while (st.hasMoreTokens())
            {
                if (st.nextToken().equalsIgnoreCase("charset"))
                {
                    try
                    {
                        encoding = st.nextToken();
                        break;
                    }
                    catch (Exception e)
                    {
                        break;
                    }
                }
            }
        }

        Reader rdr = null;

        try
        {
            // Assign a reader
            rdr = new InputStreamReader(pageConn.getInputStream(), encoding);
        }
        catch (UnsupportedEncodingException ueex)
        {
            throw new PortletException("Encoding " + encoding + " not supported. Error: " + ueex.getMessage());
        }
        catch (IOException ioex)
        {
            throw new PortletException("Failed open stream to site " + url + " Error: " + ioex.getMessage());
        }

        return rdr;
    }

    static final int BLOCK_SIZE = 4096;

    private void drain(InputStream reader, OutputStream writer) throws IOException
    {
        byte[] bytes = new byte[BLOCK_SIZE];
        try
        {
            int length = reader.read(bytes);
            while (length != -1)
            {
                if (length != 0)
                {
                    writer.write(bytes, 0, length);
                }
                length = reader.read(bytes);
            }
        }
        finally
        {
            bytes = null;
        }
    }

    private void drain(Reader r, Writer w) throws IOException
    {
        char[] bytes = new char[BLOCK_SIZE];
        try
        {
            int length = r.read(bytes);
            while (length != -1)
            {
                if (length != 0)
                {
                    w.write(bytes, 0, length);
                }
                length = r.read(bytes);
            }
        }
        finally
        {
            bytes = null;
        }

    }

    private void drain(Reader r, OutputStream os) throws IOException
    {
        Writer w = new OutputStreamWriter(os);
        drain(r, w);
        w.flush();
    }

    private Reader getRemoteReader(String uri) throws PortletException
    {
        try
        {
            HttpClient client = new HttpClient();
            GetMethod get = new GetMethod(uri);
            int status = client.executeMethod(get);
            InputStream is = get.getResponseBodyAsStream();
            // TODO need to parse HTML meta tag to get charset info
            return new InputStreamReader(is, get.getResponseCharSet());
        }
        catch (IOException e)
        {
            throw new PortletException(e);
        }
    }
    
}
