/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.axis2.scripting;

import org.apache.axis2.AxisFault;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.ServiceContext;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.receivers.AbstractInOutMessageReceiver;
import org.apache.axis2.scripting.convertors.ConvertorFactory;
import org.apache.axis2.scripting.convertors.OMElementConvertor;
import org.apache.bsf.BSFEngine;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;

/**
 * An Axis2 MessageReceiver for invoking script language functions.
 * 
 * The scripting support uses the Apache Bean Scripting Framework (BSF) 
 * so the script may be written in any language supported by BSF. 
 * 
 * There are two ways of defining the script, either in a seperate file
 * or embedded in-line within the services.xml file.
 * 
 * This example shows a services.xml using a seperate script file:
 * <code>
 *   <serviceGroup>
 *     <service ...>
 *       ...
 *       <parameter name="script">scripts/myScript.js</parameter>
 *     </service>
 *   </serviceGroup>
 * </code>
 * 
 * This example shows a JavaScript function embedded within a services.xml file:
 * <code>
 *   <serviceGroup>
 *     <service ...>
 *       ...
 *       <parameter name="script.js"><![CDATA[
 *          function invoke(inMC, outMC) {
 *             ...
 *          }
 *        ]]></parameter>
 *     </service>
 *   </serviceGroup>
 * </code>
 * 
 * The script language is determined by the file name suffix when using scripts
 * in seperate files or the script parameter name suffix when using inline scripts.
 */
public class ScriptReceiver extends AbstractInOutMessageReceiver {

    public static final String SCRIPT_ATTR = "script";
    public static final String FUNCTION_ATTR = "function";
    public static final String DEFAULT_FUNCTION = "invoke";
    public static final String CONVERTOR_ATTR = "convertor";

    protected static final String BSFENGINE_PROP = ScriptReceiver.class.getName() + "BSFEngine";
    protected static final String CONVERTOR_PROP = ScriptReceiver.class.getName() + "OMElementConvertor";
    public static final String SCRIPT_SRC_PROP = ScriptReceiver.class.getName() + "ScriptSrc";

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

    public ScriptReceiver() {
    }

    /**
     * Invokes the service by calling the script function
     */
    public void invokeBusinessLogic(MessageContext inMC, MessageContext outMC) throws AxisFault {
        try {

            log.debug("invoking script service");

            outMC.setEnvelope(getSOAPFactory(inMC).getDefaultEnvelope());

            BSFEngine engine = getBSFEngine(inMC);
            OMElementConvertor convertor = (OMElementConvertor) inMC.getServiceContext().getProperty(CONVERTOR_PROP);

            Parameter scriptFunctionParam = inMC.getAxisService().getParameter(FUNCTION_ATTR);
            String scriptFunction = scriptFunctionParam == null ? DEFAULT_FUNCTION : (String) scriptFunctionParam.getValue();

            ScriptMessageContext inScriptMC = new ScriptMessageContext(inMC, convertor);
            ScriptMessageContext outScriptMC = new ScriptMessageContext(outMC, convertor);
            Object[] args = new Object[] { inScriptMC, outScriptMC };

            engine.call(null, scriptFunction, args);

        } catch (BSFException e) {
            throw AxisFault.makeFault(e);
        }
    }

    /**
     * Gets the BSFEngine for the script service.
     * 
     * The first service invocation creates the BSFEngine and caches
     * it in the Axis2 ServiceContext for reuse by subsequent requests.
     */
    protected BSFEngine getBSFEngine(MessageContext mc) throws AxisFault {
        BSFEngine bsfEngine;
        ServiceContext serviceContext = mc.getServiceContext();
        synchronized (serviceContext) {
            bsfEngine = (BSFEngine) serviceContext.getProperty(BSFENGINE_PROP);
            if (bsfEngine == null) {
                bsfEngine = initScript(mc);
            }
        }
        return bsfEngine;
    }

    /**
     * Initializes the script service by finding the script source code,
     * compiling it in a BSFEngine, and creating an OMElementConvertor
     * for the script.
     */
    protected BSFEngine initScript(MessageContext mc) throws AxisFault {
        log.debug("initializing script service");

        AxisService axisService = mc.getAxisService();

        String scriptName = null;
        String scriptSrc = null;
        Parameter scriptFileParam = axisService.getParameter(SCRIPT_ATTR);
        if (scriptFileParam != null) {
            // the script is defined in a seperate file
            scriptName = ((String) scriptFileParam.getValue()).trim();
            Parameter scriptSrcParam = axisService.getParameter(SCRIPT_SRC_PROP);
            if (scriptSrcParam != null) {
                scriptSrc = (String) scriptSrcParam.getValue();
            } else {
                scriptSrc = readScript(axisService.getClassLoader(), scriptName);
            }
        } else {
            // the script is defined inline within the services.xml
            ArrayList<Parameter> parameters = axisService.getParameters();
            for (int i=0; scriptFileParam == null && i<parameters.size(); i++) {
                Parameter p = (Parameter) parameters.get(i);
                if (p.getName().startsWith("script.")) {
                    scriptFileParam = p;
                    scriptName = p.getName();
                    scriptSrc = (String) p.getValue();
                }
            }
        }
        if (scriptName == null) {
            throw new AxisFault("Missing script parameter");
        }
        
        try {

            String scriptLanguage = BSFManager.getLangFromFilename(scriptName);
            BSFManager bsfManager = new BSFManager();
            bsfManager.setClassLoader(BSFManager.class.getClassLoader());
            bsfManager.declareBean("_AxisService", axisService, AxisService.class);

            BSFEngine bsfEngine = bsfManager.loadScriptingEngine(scriptLanguage);
            bsfEngine.exec(scriptName, 0, 0, scriptSrc);

            ServiceContext serviceContext = mc.getServiceContext();
            serviceContext.setProperty(BSFENGINE_PROP, bsfEngine);

            OMElementConvertor convertor = ConvertorFactory.createOMElementConvertor(axisService, scriptName);
            serviceContext.setProperty(CONVERTOR_PROP, convertor);

            return bsfEngine;

        } catch (BSFException e) {
            throw AxisFault.makeFault(e);
        }
    }

    /**
     * Reads the complete script source code into a String
     */
    protected String readScript(ClassLoader cl, String scriptName) throws AxisFault {
        URL url = cl.getResource(scriptName);
        if (url == null) {
            throw new AxisFault("Script not found: " + scriptName);
        }
        InputStream is;
        try {
            is = url.openStream();
        } catch (IOException e) {
            throw new AxisFault("IOException opening script: " + scriptName, e);
        }
        try {
            Reader reader = new InputStreamReader(is, "UTF-8");
            char[] buffer = new char[1024];
            StringBuffer source = new StringBuffer();
            int count;
            while ((count = reader.read(buffer)) > 0) {
                source.append(buffer, 0, count);
            }
            return source.toString();
        } catch (IOException e) {
            throw new AxisFault("IOException reading script: " + scriptName, e);
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                throw new AxisFault("IOException closing script: " + scriptName, e);
            }
        }
    }
}
