/**
 *  Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  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.wso2.carbon.task.ui.internal;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axis2.util.XMLUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.task.TaskDescription;
import org.apache.xerces.impl.Constants;
import org.apache.xerces.util.SecurityManager;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;

/**
 * Provide utilities for Task Description Management
 */
public class TaskManagementHelper {

    private static final Log log = LogFactory.getLog(TaskManagementHelper.class);

    private static final String TASK_EXTENSION_NS = "http://www.wso2.org/products/wso2commons/tasks";

    private static final OMFactory FACTORY = OMAbstractFactory.getOMFactory();

    private static final OMNamespace TASK_OM_NAMESPACE = FACTORY.createOMNamespace(
            TASK_EXTENSION_NS, "task");

    /**
     * Factory method to create a Task Description from HttpServletResuet
     *
     * @param request HttpServletRequest instance
     * @return A Task Description
     * @throws ServletException Throws for any error during Task Description creation
     */
    public static TaskDescription createTaskDescription(
            HttpServletRequest request) throws ServletException {

        String name = request.getParameter("taskName");
        if (name == null || "".equals(name)) {
            name = request.getParameter("taskName_hidden");
            if (name == null || "".equals(name)) {
                handleException("Name cannot be null or empty");
            }
        }

        String group = request.getParameter("taskGroup");
        if (group == null || "".equals(group)) {
            group = request.getParameter("taskGroup_hidden");
            if (group == null || "".equals(group)) {
                handleException("Task group cannot be null or empty");
            }
        }

        String taskClass = request.getParameter("taskClass");
        if (taskClass == null || "".equals(taskClass)) {
            handleException("Task Class cannot be null or empty");
        }

        TaskDescription taskDescription = new TaskDescription();
        taskDescription.setName(name.trim());
        taskDescription.setTaskGroup(group.trim());
        taskDescription.setTaskImplClassName(taskClass.trim());

        String trigger = request.getParameter("taskTrigger");

        if (trigger != null && !"".equals(trigger)) {
            if ("simple".equals(trigger)) {


                String interval = request.getParameter("triggerInterval");
                if (interval != null && !"".equals(interval)) {
                    try {
                        taskDescription.setInterval(Long.parseLong(interval.trim()) * 1000);
                        taskDescription.setIntervalInMs(true);
                    } catch (NumberFormatException e) {
                        handleException("Invalid value for interval (Expected type is long) : " +
                                interval);
                    }

                }

                String count = request.getParameter("triggerCount");
                if (count != null && !"".equals(count)) {
                    try {
                        taskDescription.setCount(Integer.parseInt(count.trim()));
                    } catch (NumberFormatException e) {
                        handleException("Invalid value for Count (Expected type is int) : " +
                                count);
                    }
                }

            } else if ("cron".equals(trigger)) {

                String cron = request.getParameter("triggerCron");
                if (cron != null && !"".equals(cron)) {
                    taskDescription.setCronExpression(cron.trim());
                } else {
                    handleException("Cron expression cannot be empty for cron trigger");
                }
            }
        } else {
            handleException("No Trigger has been selected");
        }

        String pinnedServers = request.getParameter("pinnedServers");
        if (pinnedServers != null || !"".equals(pinnedServers)) {
            StringTokenizer st = new StringTokenizer(pinnedServers, " ,");
            List<String> pinnedServersList = new ArrayList<String>();
            while (st.hasMoreTokens()) {
                String token = st.nextToken();
                if (token.length() != 0) {
                    pinnedServersList.add(token);
                }
            }
            taskDescription.setPinnedServers(pinnedServersList);
        }

        String propertyCount = request.getParameter("propertyCount");
        if (propertyCount != null && !"".equals(propertyCount)) {

            try {

                int propCount = Integer.parseInt(propertyCount.trim());
                if (log.isDebugEnabled()) {
                    log.debug("Number of properties : " + propCount);
                }

                for (int i = 0; i < propCount; i++) {

                    String id = "property_name_hidden" + String.valueOf(i);
                    String propName = request.getParameter(id);

                    if (propName != null && !"".equals(propName)) {

                        String propertyType = request.getParameter("propertyTypeSelection" +
                                String.valueOf(i));

                        if ("literal".equals(propertyType)) {

                            String value = request.getParameter("textField" + String.valueOf(i));

                            if (log.isDebugEnabled()) {
                                log.debug("[ Property Name : " + name + " ]" +
                                        "[ Property Value : " + value + " ]");
                            }

                            if (value != null && !"".equals(value)) {

                                OMElement propElem = FACTORY.createOMElement("property",
                                        TASK_OM_NAMESPACE);
                                OMNamespace nullNS = FACTORY.createOMNamespace("", "");
                                propElem.addAttribute("name", propName.trim(), nullNS);
                                propElem.addAttribute("value", value.trim(), nullNS);
                                taskDescription.setXmlProperty(propElem);
                            }

                        } else if ("xml".equals(propertyType)) {

                            String value = request.getParameter("textArea" + String.valueOf(i));

                            if (log.isDebugEnabled()) {
                                log.debug("[ Property Name : " + name + " ][ Property Value : " +
                                        value + " ]");
                            }

                            if (value != null && !"".equals(value)) {

                                OMElement propElem = FACTORY.createOMElement("property",
                                        TASK_OM_NAMESPACE);
                                OMNamespace nullNS = FACTORY.createOMNamespace("", "");
                                propElem.addAttribute("name", propName.trim(), nullNS);
                                try {
                                    propElem.addChild(createOMElement(value.trim()));
                                } catch (Throwable e) {
                                    handleException("Invalid XML has been provided " +
                                            "for property : " + propName);
                                }
                                taskDescription.setXmlProperty(propElem);
                            }
                        }
                    }
                }
            } catch (NumberFormatException ignored) {
                handleException("Invalid number of properties " + propertyCount);
            }
        }
        return taskDescription;
    }

    private static OMElement createOMElement(String xml) throws Exception {
        return XMLUtils.toOM(getSecuredDocumentBuilder(true).
                parse(new ByteArrayInputStream(xml.getBytes())).getDocumentElement());
    }

    private static void handleException(String msg) throws ServletException {
        log.error(msg);
        throw new ServletException(msg);
    }

    public static TaskDescription getTaskDescription(HttpServletRequest request,
                                                     TaskManagementClient client,
                                                     String name, String group) throws Exception {
        if (name == null || "".equals(name)) {
            handleException("Task Name cannot be empty");
        } 
        name = name.trim();
        TaskDescription taskDescription = client.getTaskDescription(name, group);
        if (taskDescription == null) {
            handleException("No task description for name :" + name);
        }
        return taskDescription;
    }

    public static List<OMElement> mergeProperties(OMElement allPropertyElement,
                                                  Set<OMElement> propertySources) {

        List<OMElement> toBeReturn = new ArrayList<OMElement>();
        if (allPropertyElement == null || propertySources == null) {
            return toBeReturn;
        }

        Iterator propertyIterator = allPropertyElement.getChildElements();
        while (propertyIterator.hasNext()) {

            OMElement property = (OMElement) propertyIterator.next();
            if (property != null) {

                String name = property.getAttributeValue(new QName("", "name", ""));
                if (name != null && !"".equals(name)) {

                    OMElement element = getValueElement(propertySources, name);
                    if (element != null) {
                        toBeReturn.add(element.cloneOMElement());
                    } else {
                        toBeReturn.add(property.cloneOMElement());
                    }
                }
            }
        }
        return toBeReturn;
    }

    private static OMElement getValueElement(Set<OMElement> sources, String name) {

        for (OMElement element : sources) {
            if (element != null) {

                String elementName = element.getAttributeValue(new QName("", "name", ""));
                if (elementName != null && !"".equals(elementName)) {

                    if (elementName.trim().equals(name.trim())) {
                        return element;
                    }
                }

            }
        }
        return null;
    }

    /**
     * This method provides a secured document builder which will secure XXE attacks.
     *
     * @param setIgnoreComments whether to set setIgnoringComments in DocumentBuilderFactory.
     * @return DocumentBuilder
     * @throws javax.xml.parsers.ParserConfigurationException
     */
    public static DocumentBuilder getSecuredDocumentBuilder(boolean setIgnoreComments) throws
            ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setIgnoringComments(setIgnoreComments);
        documentBuilderFactory.setNamespaceAware(true);
        documentBuilderFactory.setExpandEntityReferences(false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        org.apache.xerces.util.SecurityManager securityManager = new SecurityManager();
        securityManager.setEntityExpansionLimit(0);
        documentBuilderFactory.setAttribute(Constants.XERCES_PROPERTY_PREFIX +
                Constants.SECURITY_MANAGER_PROPERTY, securityManager);
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        documentBuilder.setEntityResolver(new EntityResolver() {
            @Override
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                throw new SAXException("Possible XML External Entity (XXE) attack. Skipping entity resolving");
            }
        });
        return documentBuilder;
    }
}
