/*
 * Copyright (c) 2004-2020 The YAWL Foundation. All rights reserved.
 * The YAWL Foundation is a collaboration of individuals and
 * organisations who are committed to improving workflow technology.
 *
 * This file is part of YAWL. YAWL 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.
 *
 * YAWL 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 YAWL. If not, see <http://www.gnu.org/licenses/>.
 */

package org.yawlfoundation.yawl.engine.interfce.interfaceB;

import org.yawlfoundation.yawl.elements.data.external.ExternalDataGatewayFactory;
import org.yawlfoundation.yawl.elements.predicate.PredicateEvaluatorFactory;
import org.yawlfoundation.yawl.engine.ObserverGateway;
import org.yawlfoundation.yawl.engine.YEngine;
import org.yawlfoundation.yawl.engine.YSpecificationID;
import org.yawlfoundation.yawl.engine.interfce.*;
import org.yawlfoundation.yawl.engine.time.workdays.HolidayLoader;
import org.yawlfoundation.yawl.exceptions.YAWLException;
import org.yawlfoundation.yawl.exceptions.YPersistenceException;
import org.yawlfoundation.yawl.util.StringUtil;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.rmi.RemoteException;
import java.util.Date;
import java.util.Enumeration;


/**
 * Receives & responds to POST messages from custom services
 *
 * @author Lachlan Aldred
 * Date: 22/12/2003
 * Time: 12:03:41
 *
 * @author Michael Adams (refactored for v2.0, 06/2008; 12/2008)
 *
 */
public class InterfaceB_EngineBasedServer extends YHttpServlet {

    private EngineGateway _engine;
    private boolean _gatherPerfStats = false;

    public void init() throws ServletException {
        int maxWaitSeconds = 5;                             // a default

        try {
            ServletContext context = getServletContext();

            // set the path to external db gateway plugin classes (if any)
            String pluginPath = context.getInitParameter("ExternalPluginsPath");
            ExternalDataGatewayFactory.setExternalPaths(pluginPath);
            PredicateEvaluatorFactory.setExternalPaths(pluginPath);

            // init engine reference
            _engine = (EngineGateway) context.getAttribute("engine");
            if (_engine == null) {
                Class<? extends YEngine> engineImpl = getEngineImplClass();
                boolean persist = getBooleanFromContext("EnablePersistence");
                boolean enableHbnStats = getBooleanFromContext("EnableHibernateStatisticsGathering");
                boolean redundantMode = getBooleanFromContext("StartInRedundantMode");
                _engine = new EngineGatewayImpl(engineImpl, persist,
                        enableHbnStats, redundantMode);
                _engine.setActualFilePath(context.getRealPath("/"));
                context.setAttribute("engine", _engine);
            }

            // enable performance statistics gathering if requested
            _gatherPerfStats = getBooleanFromContext("EnablePerformanceStatisticsGathering");

            // set flag to disable logging (only if false) - enabled with persistence by
            // default
            String logStr = context.getInitParameter("EnableLogging");
            if ((logStr != null) && logStr.equalsIgnoreCase("false")) {
                _engine.disableLogging();
            }

            // add the reference to the default worklist
            _engine.setDefaultWorklist(context.getInitParameter("DefaultWorklist"));

            // set flag for generic admin account (only if set to true)
            String allowAdminID = context.getInitParameter("AllowGenericAdminID");
            if ((allowAdminID != null) && allowAdminID.equalsIgnoreCase("true")) {
                _engine.setAllowAdminID(true);
            }

            // override the max time that initialisation events wait for between
            // final engine init and server start completion
            int maxWait = StringUtil.strToInt(
                    context.getInitParameter("InitialisationAnnouncementTimeout"), -1);
            if (maxWait >= 0) maxWaitSeconds = maxWait;

            // set the country/region codes used for calculating work-day-only timers (if any)
            String timerLocationConfig = context.getInitParameter("WorkdayTimerGeoCodes");
            if (timerLocationConfig != null) {
                new HolidayLoader(false).startupCheck(timerLocationConfig);
            }

            // read the current version properties
            _engine.initBuildProperties(context.getResourceAsStream(
                               "/WEB-INF/classes/version.properties"));

            // init any 3rd party observer gateways
            String gatewayStr = context.getInitParameter("ObserverGateway");
            if (gatewayStr != null) {

                // split multiples on the semi-colon (if any)
                for (String gateway : gatewayStr.split(";")) {
                    registerObserverGateway(gateway);
                }
            }
        }
        catch (YPersistenceException e) {
            _log.fatal("Failure to initialise runtime (persistence failure)", e);
            throw new UnavailableException("Persistence failure");
        }

        if (_engine != null) {
            _engine.notifyServletInitialisationComplete(maxWaitSeconds);
        }
        else {
            _log.fatal("Failed to initialise Engine (unspecified failure). Please " +
                    "consult the logs for details");
            throw new UnavailableException("Unspecified engine failure");
        }
    }


    private void registerObserverGateway(String gatewayClassName) {
        ObserverGateway gateway ;
        try {
            Class gatewayClass = Class.forName(gatewayClassName);

            // If the class has a getInstance() method, call that method rather than
            // calling a constructor (& thus instantiating 2 instances of the class)
            try {
                Method instMethod = gatewayClass.getDeclaredMethod("getInstance");
                gateway = (ObserverGateway) instMethod.invoke(null);
            }

            // no getInstance(), so just create a plain new instance
            catch (NoSuchMethodException nsme) {
                gateway = (ObserverGateway) gatewayClass.newInstance();
            }

            if (gateway != null)
                _engine.registerObserverGateway(gateway);
            else
                _log.warn("Error registering external ObserverGateway '{}'.",
                        gatewayClassName);
        }
        catch (ClassNotFoundException e) {
            _log.warn("Unable to locate external ObserverGateway '" +
                    gatewayClassName + "'.", e);
        }
        catch (InstantiationException ie) {
            _log.warn("Unable to instantiate external ObserverGateway '" +
                    gatewayClassName +
                    "'. Perhaps it is missing a no-argument constructor.", ie);
        }
        catch (YAWLException ye) {
            _log.warn("Failed to register external ObserverGateway '" +
                    gatewayClassName + "'.", ye);
        }
        catch (Exception e) {
            _log.warn("Unable to instantiate external ObserverGateway '" +
                    gatewayClassName + "'.", e);
        }
    }


    private Class<? extends YEngine> getEngineImplClass() {
        String implClassName = getServletContext().getInitParameter("EngineImpl");
        if (! StringUtil.isNullOrEmpty(implClassName)) {
            try {
                Class c = Class.forName(implClassName);
                if (YEngine.class.isAssignableFrom(c)) {
                    return (Class<? extends YEngine>) c;
                }
                _log.warn("Class '{}' is not a superclass of YEngine.", implClassName);
            }
            catch (ClassNotFoundException e) {
                 _log.warn("Unable to locate external YEngine class '" +
                         implClassName + "'.", e);
            }
            _log.warn("Reverting to the default YEngine implementation.");
        }
        return null;
    }


    public void destroy() {
        _engine.shutdown();
        super.destroy();
    }


    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if ("HEAD".equals(request.getMethod())) return;
        doPost(request, response);                 // redirect all GETs to POSTs
    }


    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

        OutputStreamWriter outputWriter = ServletUtils.prepareResponse(response);
        StringBuilder output = new StringBuilder();
        output.append("<response>");
        output.append(processPostQuery(request));
        output.append("</response>");
        if (_engine.enginePersistenceFailure())
        {
            _log.fatal("************************************************************");
            _log.fatal("A failure has occurred whilst persisting workflow state to the");
            _log.fatal("database. Check the status of the database connection defined");
            _log.fatal("for the YAWL service, and restart the YAWL web application.");
            _log.fatal("Further information may be found within the Tomcat log files.");
            _log.fatal("************************************************************");
            response.sendError(500, "Database persistence failure detected");
        }
        outputWriter.write(output.toString());
        outputWriter.flush();
        outputWriter.close();
        //todo find out how to provide a meaningful 500 message in the format of  a fault message.
    }


    //###############################################################################
    //      Start YAWL Processing methods
    //###############################################################################

    private String processPostQuery(HttpServletRequest request) {
        StringBuilder msg = new StringBuilder();
        String sessionHandle = request.getParameter("sessionHandle");
        String action = request.getParameter("action");
        String workItemID = request.getParameter("workItemID");
        String specIdentifier = request.getParameter("specidentifier");
        String specVersion = request.getParameter("specversion");
        String specURI = request.getParameter("specuri");
        String taskID = request.getParameter("taskID");
        long start = System.nanoTime();
        
        try {
            debug(request, "Post");
            if (_engine.isRedundantMode() && ! isAllowedRedundantAction(action)) {
                return fail("Unable to process request - engine is in redundant mode");
            }

            if (action != null) {
                if (action.equals("checkConnection")) {
                    msg.append(_engine.checkConnection(sessionHandle));
                }
                else if (action.equals("connect")) {
                    String userID = request.getParameter("userid");
                    String password = request.getParameter("password");
                    int interval = request.getSession().getMaxInactiveInterval();
                    msg.append(_engine.connect(userID, password, interval));
                }
                else if ("disconnect".equals(action)) {
                    msg.append(_engine.disconnect(sessionHandle));
                }
                else if (action.equals("checkout")) {
                    msg.append(_engine.startWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("checkin")) {
                    String data = request.getParameter("data");
                    String logPredicate = request.getParameter("logPredicate");
                    msg.append(_engine.completeWorkItem(workItemID, data, logPredicate, false,
                            sessionHandle));
                }
                else if (action.equals("rejectAnnouncedEnabledTask")) {
                    msg.append(_engine.rejectAnnouncedEnabledTask(workItemID, sessionHandle));
                }
                else if (action.equals("launchCase")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    URI completionObserver = getCompletionObserver(request);
                    String caseParams = request.getParameter("caseParams");
                    String logDataStr = request.getParameter("logData");
                    String caseID = request.getParameter("caseid");
                    String mSecStr = request.getParameter("mSec");
                    String startStr = request.getParameter("start");
                    String waitStr = request.getParameter("wait");
                    if (mSecStr != null) {
                        msg.append(_engine.launchCase(specID, caseParams,
                                   completionObserver, logDataStr,
                                   StringUtil.strToLong(mSecStr, 0), sessionHandle));
                    }
                    else if (startStr != null) {
                        long time = StringUtil.strToLong(startStr, 0);
                        Date date = time > 0 ? new Date(time) : new Date();
                        msg.append(_engine.launchCase(specID, caseParams,
                                   completionObserver, logDataStr, date, sessionHandle));
                    }
                    else if (waitStr != null) {
                        msg.append(_engine.launchCase(specID, caseParams,
                                   completionObserver, logDataStr,
                                   StringUtil.strToDuration(waitStr), sessionHandle));
                    }
                    else if (caseID != null) {
                        msg.append(_engine.launchCase(specID, caseParams,
                                completionObserver, caseID, logDataStr, sessionHandle));
                    }
                    else msg.append(_engine.launchCase(specID, caseParams,
                                    completionObserver, logDataStr, sessionHandle));
                }
                else if (action.equals("cancelCase")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.cancelCase(caseID, sessionHandle));
                }
                else if (action.equals("getWorkItem")) {
                    msg.append(_engine.getWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("startOne")) {
                    String userID = request.getParameter("user");
                    msg.append(_engine.startWorkItem(userID, sessionHandle));
                }
                else if (action.equals("getLiveItems")) {
                    msg.append(_engine.describeAllWorkItems(sessionHandle));
                }
                else if (action.equals("getAllRunningCases")) {
                    msg.append(_engine.getAllRunningCases(sessionHandle));
                }
                else if (action.equals("getWorkItemsWithIdentifier")) {
                    String idType = request.getParameter("idType");
                    String id = request.getParameter("id");
                    msg.append(_engine.getWorkItemsWithIdentifier(idType, id, sessionHandle));
                }
                else if (action.equals("getWorkItemsForService")) {
                    String serviceURI = request.getParameter("serviceuri");
                    msg.append(_engine.getWorkItemsForService(serviceURI, sessionHandle));
                }
                else if (action.equals("taskInformation")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getTaskInformation(specID, taskID, sessionHandle));
                }
                else if (action.equals("getMITaskAttributes")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getMITaskAttributes(specID, taskID, sessionHandle));
                }
                else if (action.equals("getResourcingSpecs")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getResourcingSpecs(specID, taskID, sessionHandle));
                }
                else if (action.equals("checkIsAdmin")) {
                    msg.append(_engine.checkConnectionForAdmin(sessionHandle));
                }
                else if (action.equals("checkAddInstanceEligible")) {
                    msg.append(_engine.checkElegibilityToAddInstances(
                                                              workItemID, sessionHandle));
                }
                else if (action.equals("getSpecificationPrototypesList")) {
                    msg.append(_engine.getSpecificationList(sessionHandle));
                }
                else if (action.equals("getSpecification")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getProcessDefinition(specID, sessionHandle));
                }
                else if (action.equals("getSpecificationData")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getSpecificationData(specID, sessionHandle));
                }
                else if (action.equals("getSpecificationDataSchema")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getSpecificationDataSchema(specID, sessionHandle));
                }
                else if (action.equals("getCasesForSpecification")) {
                    YSpecificationID specID =
                            new YSpecificationID(specIdentifier, specVersion, specURI);
                    msg.append(_engine.getCasesForSpecification(specID, sessionHandle));
                }
                else if (action.equals("getSpecificationForCase")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getSpecificationForCase(caseID, sessionHandle));
                }
                else if (action.equals("getSpecificationIDForCase")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getSpecificationIDForCase(caseID, sessionHandle));
                }
                else if (action.equals("getCaseState")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getCaseState(caseID, sessionHandle));
                }
                else if (action.equals("exportCaseState")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.exportCaseState(caseID, sessionHandle));
                }
                else if (action.equals("exportAllCaseStates")) {
                    msg.append(_engine.exportAllCaseStates(sessionHandle));
                }
                else if (action.equals("importCases")) {
                    String xml = request.getParameter("xml");
                    msg.append(_engine.importCases(xml, sessionHandle));
                }
                else if (action.equals("getCaseData")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getCaseData(caseID, sessionHandle));
                }
                else if (action.equals("getChildren")) {
                    msg.append(_engine.getChildrenOfWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("getWorkItemExpiryTime")) {
                    msg.append(_engine.getWorkItemExpiryTime(workItemID, sessionHandle));
                }
                else if (action.equals("getCaseInstanceSummary")) {
                    msg.append(_engine.getCaseInstanceSummary(sessionHandle));
                }
                else if (action.equals("getWorkItemInstanceSummary")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getWorkItemInstanceSummary(caseID, sessionHandle));
                }
                else if (action.equals("getParameterInstanceSummary")) {
                    String caseID = request.getParameter("caseID");
                    msg.append(_engine.getParameterInstanceSummary(caseID, workItemID, sessionHandle));
                }
                else if (action.equals("createInstance")) {
                    String paramValueForMICreation =
                            request.getParameter("paramValueForMICreation");
                    msg.append(_engine.createNewInstance(workItemID,
                            paramValueForMICreation, sessionHandle));
                }
                else if (action.equals("suspend")) {
                    msg.append(_engine.suspendWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("rollback")) {
                    msg.append(_engine.rollbackWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("unsuspend")) {
                    msg.append(_engine.unsuspendWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("skip")) {
                    msg.append(_engine.skipWorkItem(workItemID, sessionHandle));
                }
                else if (action.equals("getStartingDataSnapshot")) {
                    msg.append(_engine.getStartingDataSnapshot(workItemID, sessionHandle));
                }
                else if (action.equals("pollPerfStats")) {
                    msg.append(PerfReporter.poll());
                }
                if (_gatherPerfStats) PerfReporter.add(action, start);
            }  // action is null
            else if (request.getRequestURI().endsWith("ib")) {
                msg.append(_engine.getAvailableWorkItemIDs(sessionHandle));
            }
            else if (request.getRequestURI().contains("workItem")) {
                msg.append(_engine.getWorkItemOptions(workItemID,
                        request.getRequestURL().toString(), sessionHandle));
            }
            else _log.error("Interface B called with null action.");
        }
        catch (RemoteException e) {
            _log.error("Remote Exception in Interface B with action: " + action, e);
        }
        _log.debug("InterfaceB_EngineBasedServer::doPost() result = {}", msg);
        return msg.toString();
    }


    private URI getCompletionObserver(HttpServletRequest request) {
        String completionObserver = request.getParameter("completionObserverURI");
        if(completionObserver != null) {
            try {
                return new URI(completionObserver);
            } catch (URISyntaxException e) {
                _log.error("Failure to ", e);
            }
        }
        return null;
    }                                         


    private void debug(HttpServletRequest request, String service) {
        if (_log.isDebugEnabled()) {
            _log.debug("\nInterfaceB_EngineBasedServer::do{}() request.getRequestURL={}",
                    service, request.getRequestURL());
            _log.debug("\nInterfaceB_EngineBasedServer::do{}() request.parameters:", service);
            Enumeration paramNms = request.getParameterNames();
            while (paramNms.hasMoreElements()) {
                String name = (String) paramNms.nextElement();
                _log.debug("\trequest.getParameter({}) = {}", name, request.getParameter(name));
            }
        }
    }

}