/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2005 Bull S.A.
 * 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: ApplyArchiveConfigAction.java 16121 2009-01-07 16:17:40Z eyindanga $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.webapp.jonasadmin.xml;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import javax.management.ObjectName;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;

import org.ow2.jonas.lib.management.extensions.base.JonasManagementRepr;
import org.ow2.jonas.lib.util.XMLSerializer;
import org.ow2.jonas.webapp.jonasadmin.JonasAdminJmx;
import org.ow2.jonas.webapp.jonasadmin.JonasBaseAction;
import org.ow2.jonas.webapp.jonasadmin.xml.xs.ElementRestrictions;
import org.ow2.jonas.webapp.jonasadmin.xml.xs.SchemaRestrictions;
import org.ow2.jonas.webapp.jonasadmin.xml.xs.SchemaRestrictionsFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;

/**
 * Action to process the input when either the advanced or the form view of the
 * archive configuration feature is submitted.
 *
 * If the user selected the 'Apply' button the action validates and saves the
 * updated deployment descriptor.
 *
 * If the user selected the 'Switch to Form/Advanced" the input is validated
 * updated in the form and the user is forwarded to the other view.
 *
 * @author Patrick Smith
 * @author Gregory Lapouchnian
 */
public class ApplyArchiveConfigAction extends JonasBaseAction {

    /**
     * Executes the struts action.
     *
     * @param p_Mapping the struts action mapping.
     * @param p_Form the struts action form.
     * @param p_Request the HttpServletRequest.
     * @param p_Response the HttpServletResponse.
     * @throws IOException
     * @throws ServletException
     * @return the action forward to forward to.
     */
    public ActionForward executeAction(ActionMapping p_Mapping,
            ActionForm p_Form, HttpServletRequest p_Request,
            HttpServletResponse p_Response) throws IOException,
            ServletException {

        // Form used
        ArchiveConfigForm oForm = (ArchiveConfigForm) p_Form;

        // Current JOnAS server
        String serverName = m_WhereAreYou.getCurrentJonasServerName();
        String domainName = m_WhereAreYou.getCurrentDomainName();
        /**
         * Where the user should be forwarded to display the confirmation page.
         */
        String confirmForward = "Archive Config Confirm";
        /**
         * Where the user should be returned to when an error occurs.
         */
        String currentPageForward = getCurrentPageForward(oForm);
        /**
         * Where the user should be forwarded to in case of switching views.
         */
        String switchForward = getSwitchForward(oForm);

        /**
         * The forward to the archive selection page.
         */
        String selectForward = "Archive Config Select";

        // if the operation was cancelled
        if (oForm.getCancel() != null) {
            oForm.setCancel(null);
            return (p_Mapping.findForward(selectForward));
        }

        try {
            if (oForm.getConfigType() == ArchiveConfigForm.FORM) {
                processFormInput(oForm, p_Mapping, p_Request, domainName, serverName);
            } else {
                processTextAreaInput(oForm, p_Mapping, p_Request, domainName, serverName);
            }
        } catch (Exception e) {
            return reportError(currentPageForward, oForm, "An error occured when procesing the input. " + e.getMessage(),
                    p_Mapping, p_Request);
        }

        // if we are switching to another view we don't try to save the document
        if (oForm.getSwitchTo() != null) {

            // reset the switch button value
            oForm.setSwitchTo(null);

            return (p_Mapping.findForward(switchForward));
        }

        // reset the submit button value
        oForm.setSubmit(null);


        try {
            saveChanges(oForm, domainName, serverName);
        } catch (Exception e) {
            return reportError(currentPageForward, oForm, "An error occured when trying to save changed. " + e.getMessage(),
                    p_Mapping, p_Request);
        }


        // Forward to the confirm jsp.
        return (p_Mapping.findForward(confirmForward));
    }

    private void processFormInput(ArchiveConfigForm oForm,
            ActionMapping p_Mapping, HttpServletRequest p_Request, String domainName, String serverName)
            throws Exception {
        // used to keep track of added and removed elements while processing
        // form input
        ArrayList addedList = new ArrayList();
        ArrayList removedList = new ArrayList();

        Map mappings = oForm.getMapping();
        Map values = oForm.getValuesMap();

        Iterator i = values.keySet().iterator();
        while (i.hasNext()) {

            String key = (String) i.next();

            if (mappings.containsKey(key)) {
                Node node = ((Node) mappings.get(key));

                // wasn't removed, just need to update the values
                if (!values.containsKey(key + "-removed")) {
                    node.setNodeValue((String) values.get(key));
                } else { // this node was removed by the user
                        node.getParentNode().removeChild(node);
                        removedList.add(key);
                }

            } else if (values.containsKey(key + "-parent")) {
                String parentID = (String) values.get(key + "-parent");
                String name = (String) values.get(key + "-name");
                SchemaRestrictions restrictions = (new SchemaRestrictionsFactory())
                        .getSchemaRestrictions(SchemaRestrictions.RAR_TYPE);


                    Node parentNode = (Node) mappings.get(parentID);
                    // if we need to insert the child in a specific location
                    ElementRestrictions er = restrictions
                            .getElementRestrictions(parentNode
                                    .getNodeName());

                    Element newElement = ((Document) oForm.getDocument())
                        .createElementNS(parentNode.getNamespaceURI(), name);
                    if (!restrictions.getComplexElements().contains(name)) {
                        newElement.appendChild(oForm.getDocument()
                                .createTextNode((String) values.get(key)));
                    }

                    if (er.isSequence()) {
                        int elIndex = er.getChildren().indexOf(newElement.getNodeName());

                        if(parentNode.getChildNodes().getLength() == 0 || elIndex == er.getChildren().size()) {
                            parentNode.appendChild(newElement);
                        }
                        else {
                            int insertionIndex = findInsertionIndex(er, parentNode.getChildNodes(), newElement);

                            if(insertionIndex < 0) {
                                parentNode.appendChild(newElement);
                            }
                            else {
                                String insertBefore = (String)er.getChildren().get(insertionIndex);
                                for(int j = 0; j < parentNode.getChildNodes().getLength(); j++) {
                                    Node current = parentNode.getChildNodes().item(j);
                                    if (current.getNodeName().equals(insertBefore)) {
                                        parentNode.insertBefore(newElement, current);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    else {
                        // not a sequence, we can simply append
                        parentNode.appendChild(newElement);
                    }
                    mappings.put(key, newElement);
                    addedList.add(key);
            }
            // else nothing to update for this key.
        }

        // Remove the '-parent' and '-name' keys and values of the
        // elements that have been added to the document.
        for (int k = 0; k < addedList.size(); k++) {
            values.remove(addedList.get(k) + "-parent");
            values.remove(addedList.get(k) + "-name");
        }

        // Remove the '-removed' keys and values of the elements
        // that have been removed from the document.
        for (int k = 0; k < removedList.size(); k++) {
            values.remove(removedList.get(k) + "-removed");
        }

        // remove occurences of &#xd; that appear after serializing
        // the document
        String xml = serializeDocument(oForm.getDocument());
        xml = xml.replaceAll("&#xd;", "");
        // update the value of the raw XML in the form
        oForm.setXml(xml);

        validateDocument(oForm.getDocument(), domainName, serverName);
    }

    private int findInsertionIndex(ElementRestrictions er, NodeList list, Element newElement) {
        ArrayList a = new ArrayList();
        for(int i = 0; i < list.getLength(); i++) {
            if(list.item(i).getNodeType() == Node.ELEMENT_NODE) {
                a.add(new Integer(er.getChildren().indexOf(list.item(i).getNodeName())));
            }
        }
        Integer newIndex = new Integer(er.getChildren().indexOf(newElement.getNodeName()));
        a.add(newIndex);

        Collections.sort(a);
        int result = a.indexOf(newIndex);
        if(result == a.size()-1)
            return -1;
        else
            return ((Integer)a.get(result+1)).intValue();
    }

    /**
     * Save the modified document.
     * @param oForm the Struts from which stores the document.
     */
    private void saveChanges(ArchiveConfigForm oForm, String domainName, String serverName) throws Exception {
        // we are not switching views, but want to save the changes
        ObjectName on = JonasAdminJmx.getRarConfigObjectName(domainName, serverName);

        Object[] params = new Object[] { oForm.getArchiveName(),
                                         oForm.getPathName(),
                                         oForm.getDocument() };

        String[] sig = new String[] { "java.lang.String",
                                      "java.lang.String",
                                      "org.w3c.dom.Document" };

        JonasManagementRepr.invoke(on, "saveXML", params, sig, serverName);
    }

    /**
     * Processes the updated XML passed from the <textarea>
     * component and updates the Document representation.
     *
     * @param oForm the Struts form
     * @param p_Mapping the struts action mapping.
     * @param p_Request the HttpServletRequest.
     * @throws Exception if the XML is not well-formed or not valid
     */
    private void processTextAreaInput(ArchiveConfigForm oForm,
            ActionMapping p_Mapping, HttpServletRequest p_Request, String domainName, String serverName)
            throws Exception {
        String xml = oForm.getXml();

        ByteArrayInputStream str = new ByteArrayInputStream(xml.getBytes());
        Document doc;

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();

        // Get the entity resolver for RAR DDs from the MBean
        EntityResolver jer;
        ObjectName on = JonasAdminJmx.getRarConfigObjectName(domainName, serverName);
        jer = (EntityResolver) JonasManagementRepr.invoke(on,
                "getEntityResolver", new Object[] {}, new String[] {}, serverName);

        builder.setEntityResolver(jer);
        doc = builder.parse(str);
        str.close();

        // set the document to the Document object just parsed from the
        // textarea input
        oForm.setDocument(doc);

        validateDocument(oForm.getDocument(), domainName, serverName);
    }

    /**
     * Get the ActionForward name for the current page.
     * @param form Struts form
     * @return ActionForward for the current page
     */
    protected String getCurrentPageForward(ArchiveConfigForm form) {
        if (form.getConfigType() == ArchiveConfigForm.FORM) {
            return "Archive Config";
        } else {
            return "Archive Config Advanced";
        }
    }

    /**
     * Get the ActionForward name to switch views from the current page.
     * @param form Struts form
     * @return ActionForward the user should be forwarded to if they
     * are switching views
     */
    protected String getSwitchForward(ArchiveConfigForm form) {
        if (form.getConfigType() == ArchiveConfigForm.FORM) {
            return "Archive Config Advanced";
        } else {
            return "Archive Config";
        }
    }

    /**
     * Report an error and forward to the given JSP page.
     *
     * @param sForward what JSP page to forward to
     * @param oForm the form bean
     * @param errorMessage the message from the exception
     * @param p_Mapping
     * @param p_Request
     * @return ActionForward found for the given forward string
     */
    protected ActionForward reportError(String sForward,
            ArchiveConfigForm oForm, String errorMessage,
            ActionMapping p_Mapping, HttpServletRequest p_Request) {

        m_Errors.add("error.archiveconfig.valid.fail", new ActionMessage(
                "error.archiveconfig.valid.fail", errorMessage));
        saveErrors(p_Request, m_Errors);

        // reset the values fo the buttons
        oForm.setSwitchTo(null);
        oForm.setSubmit(null);

        // forward back to the advanced view to display the errors
        return (p_Mapping.findForward(sForward));
    }

    /**
     * Serialize a DOM tree into plaint text.
     *
     * @param doc Document tree to convert
     * @return a string representation of the DOM tree
     */
    protected String serializeDocument(Document doc) {
        XMLSerializer ser = new XMLSerializer(doc);

        OutputStream out = new java.io.ByteArrayOutputStream();
        try {
            ser.serialize(out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return out.toString();
    }

    /**
     * Validate the document using appropriate schemas.
     *
     * @param doc the document that needs to be validated
     * @throws Exception an exception if the document is not valid
     */
    protected void validateDocument(Document doc, String domainName, String serverName) throws Exception {
        ObjectName on = JonasAdminJmx.getRarConfigObjectName(domainName, serverName);
        Object[] params = new Object[] { doc };
        String[] sig = new String[] { "org.w3c.dom.Document" };

        JonasManagementRepr.invoke(on, "verifyDocument", params, sig, serverName);
    }
}
