/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.bebop.jsp;

import com.arsdigita.bebop.Component;
import com.arsdigita.bebop.Page;
import com.arsdigita.bebop.PageFactory;
import com.arsdigita.bebop.PageState;
import com.arsdigita.bebop.SlaveComponent;
import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.util.UncheckedWrapperException;
import com.arsdigita.xml.Document;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTag;
import javax.servlet.jsp.tagext.Tag;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;

/**
 * Defines a Bebop page with JSP tags.  Component tags within the page
 * will add components to the Page.  At the end of the tag, we generate
 * XML output from the Bebop page and render it with the designated
 * PresentationManager.
 * <p>If no presentation manager is supplied, then the output XML
 * document from the page definition is just stored in the
 * "com.arsdigita.xml.Document" request attribute.
 * <p>You can also specify a base class for the Page object defined, using
 * the "pageClass" attribute.
 *
 * <p>Example usage:
 * <pre>&lt;define:page name="p" [title="title"] [pmClass="..."] [pageClass=...]>
 *   ... define components here ...
 * &lt;/bebop:page>
 * </pre>
 *
 * <p><b>Note on Bebop static/dynamic split</b>: You should not assume
 * that any code inside define:page will be executed more than once.
 * The created Page object may be cached.
 */
public class DefinePage extends DefineContainer implements JSPConstants {

    public static final String versionId = "$Id: //core-platform/dev/src/com/arsdigita/bebop/jsp/DefinePage.java#21 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

    private Page m_page;
    private String m_title;
    private String m_application;
    private String m_master;
    private Class m_pageClass;
    private boolean m_cache = true;
    private static final Logger s_log =
        Logger.getLogger(DefinePage.class.getName());

    private static Map s_pageCache = new HashMap();
    // maps URL(/packages/foo/www/asdf.jsp) -> {Page, creation date}
    private static Map s_pageLocks = new HashMap();

    /**
     * Creates a Bebop Page instance.  A page tag is a special case
     * because we don't expect it to have a parent tag.
     */
    public int doStartTag() throws JspException {
        if (m_cache) {
            String cacheKey = DispatcherHelper.getCurrentResourcePath
                ((HttpServletRequest)pageContext.getRequest());
            Object pageLock;
            // First off all we have the global synchronization 
            // block to get the page's unique sync object.
            synchronized (s_pageLocks) {
                pageLock = s_pageLocks.get(cacheKey);
                if (pageLock == null) {
                    pageLock = cacheKey;
                    s_pageLocks.put(cacheKey, cacheKey);
                }
            }
            // Now we just synchronize against our specific page.
            synchronized (pageLock) {
                Object[] pair = (Object[])s_pageCache.get(cacheKey);
                if (pair != null) {
                    long pageDate = ((Long)pair[1]).longValue();
                    File jspFile = new File(pageContext.getServletContext()
                                            .getRealPath(cacheKey));
                    if (jspFile.lastModified() <= pageDate) {
                        // jsp file is not newer than cached page, so we can use
                        // the cached page.
                        Page page = (Page)pair[0];
                        
                        // We may have to for the page to be locked
                        // by another thread
                        while (!page.isLocked()) {
                            if (s_log.isDebugEnabled()) {
                                s_log.debug(Thread.currentThread().getName() + " waiting on " + cacheKey);
                            }
                            try {
                                pageLock.wait(500);
                            } catch (InterruptedException ex) {
                                if (s_log.isDebugEnabled()) {
                                    s_log.debug(Thread.currentThread().getName() + " waiting interrupted on " + cacheKey, ex);
                                }
                                continue;
                            }
                        }
                    }
                    m_page = (Page)pair[0];
                    pageContext.setAttribute(getName(), m_page);
                    return SKIP_BODY;
                }
                
                m_page = buildPage();
                s_pageCache.put(cacheKey,
                                new Object[] {m_page,
                                              new Long(System.currentTimeMillis())});
            }
        } else {
            m_page = buildPage();
        }

        return EVAL_BODY_BUFFERED;
    }

    private Page buildPage() throws JspException {
        Page page;

        if (m_pageClass == null && m_application == null) {
            m_pageClass = Page.class;
        }

        if (m_pageClass == null) {
            page = PageFactory.buildPage(m_application,
                                           m_title,
                                           getName());
        } else {
            try {
                page = (Page)m_pageClass.newInstance();
            } catch (IllegalAccessException e) {
                throw new JspWrapperException("cannot create page class instance", e);
            } catch (InstantiationException e) {
                throw new JspWrapperException("cannot create page class instance", e);
            }
            if (m_title != null) {
                page.setTitle(m_title);
            }
        }
        pageContext.setAttribute(getName(), page);

        // we *might* be nested, but probably aren't
        Tag t = getParent();
        if (t != null && t instanceof DefineContainer) {
            ((BodyTag)t).doAfterBody();
        }

        return page;
    }

    /**
     * Locks the Page object, generates XML from the Page,
     * gets a presentation manager instance, and renders the XML using
     * the XSLT transformation in the PresentationManager.
     *
     * <p> Nested pages are special cases; if this define:page is nested
     * in another define:page tag, then we won't generate output from
     * this Page directly but we'll make a reference from the parent
     * Page into this page.  Master/slave works similarly; we pass
     * the current page object to the master JSP file, which will include
     * the current page as the slave page to include at the point where
     * &lt;define:slave/> appears.
     *
     */
    public int doEndTag() throws JspException {
        try {
            if (m_cache) {
                String cacheKey = DispatcherHelper.getCurrentResourcePath
                    ((HttpServletRequest)pageContext.getRequest());
                Object pageLock;
                // Global lock to get hold of page sync object
                synchronized (s_pageLocks) {
                    pageLock = s_pageLocks.get(cacheKey);
                    if (pageLock == null) {
                        pageLock = cacheKey;
                        s_pageLocks.put(cacheKey, cacheKey);
                    }
                }
                // Now sync just on this page
                synchronized(pageLock) {
                    if (!m_page.isLocked()) {
                        m_page.lock();

                        // Now notify anyone waiting for us to lock the page
                        // that we're all done. see doStartTag()
                        if (s_log.isDebugEnabled()) {
                            s_log.debug(Thread.currentThread().getName() + " waking everyone up on " + cacheKey);
                        }
                        pageLock.notifyAll();
                    }
                }
            } else {
                if (!m_page.isLocked()) {
                    m_page.lock();
                }
            }
            Tag t = getParent();
            if (t != null && t instanceof DefineContainer) {
                // nested...
                // treat like a slave component, a page nested
                // within another page
                ((DefineContainer)t).addComponent(new SlaveComponent(m_page));
                // if we're nested
            } else if (m_master != null) {
                // if we have a master page, then we pass control to it,
                // with the current (m_page) as a request attribute.
                pageContext.getRequest()
                    .setAttribute("com.arsdigita.bebop.SlavePage", m_page);
                DispatcherHelper.forwardRequestByPath(m_master, pageContext);
            } else {
                // pass on the document to the ShowAll code. Note ugly
                // long-range code dependency voodoo here.
                HttpServletRequest req =
                    (HttpServletRequest)pageContext.getRequest();
                HttpServletResponse resp =
                    (HttpServletResponse)pageContext.getResponse();
                
                Document doc;
                PageState state;
          
                try {
                    doc = new Document ();
                    state = m_page.process (req, resp);
                }
                catch (ParserConfigurationException ex) {
                    throw new UncheckedWrapperException (ex);
                }
                catch (ServletException ex) {
                    throw new UncheckedWrapperException (ex);
                }
                m_page.generateXML (state, doc);

                req.setAttribute(INPUT_DOC_ATTRIBUTE, doc);
                pageContext.setAttribute(INPUT_PAGE_STATE_ATTRIBUTE, state);
            }
            return EVAL_PAGE;
        } catch (Exception e) {
            try {
                // try to serve <%@ page errorPage=... %>
                pageContext.handlePageException(e);
            } catch (Throwable nested) {
                // serving error page failed, so
                // percolate error up to top-level
                throw new UncheckedWrapperException(e);
            }
            return SKIP_PAGE;
        }
    }

    protected final Component getComponent() {
        return m_page;
    }

    public final void setTitle(String s) {
        m_title = s;
    }

    public final void setApplication(String s) {
        m_application = s;
    }

    public void setPageClass(String s) throws JspException {
        try {
            m_pageClass = Class.forName(s);
        } catch (ClassNotFoundException e) {
            throw new JspException(e.toString());
        }
    }

    public final void setMaster(String s) {
        m_master = s;
    }

    public void setCache(String s) {
        m_cache = new Boolean(s).booleanValue();
    }
}
