/* Copyright (c) 2002, Oracle Corportation. All rights reserved */
package oracle.portal.provider.v2.webservice;

import java.lang.reflect.Method;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.math.BigDecimal;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Comment;
import oracle.webdb.utils.SimpleStringBuffer;
import oracle.portal.utils.xml.v2.NodeHandlerException;
import oracle.portal.provider.v2.PortletException;
import oracle.portal.provider.v2.ProviderSession;

/**
 * <code>RPCWebServiceRenderer</code> is a specialized {@link
 * WebServiceRenderer} for invoking 'RPC style' web services. An RPC style web
 * service communicates using a specialized syntax to encode remote procedure
 * calls and can be identified from a <code>&lt;soap:binding
 * style="rpc"></code> declaration in a WSDL (Web Services Description
 * Language) interface description. The request to the web service enodes an
 * argument list to a method to be called on the remote service, while its
 * response encodes the method's return value or an exception if an error
 * occurs.<p>
 *
 * <code>RPCWebServiceRenderer</code> builds on the automated support for RPC
 * style web services built in to Oracle JDeveloper version 9.0.2 and later. It
 * assumes that you have used JDeveloper to analyze the web service's WSDL
 * document and automatically generate a 'stub' or 'proxy' class for the web
 * service which translates from Java method calls into remote procedure calls
 * on the web service. Consult the JDeveloper section of <a
 * href="http://otn.oracle.com">otn.oracle.com</a> to find out how to download
 * JDeveloper, and consult the JDeveloper online help for details of how to
 * generate Web Service Stubs.<p>
 *
 * Note that as well as using the <code>param</code> property to define simple
 * parameters to a web service method, the <code>literal</code> property may be
 * used to defined parameter bindings to a method which accepts one or more
 * <code>org.w3c.dom.Element</code> objects.
 */
public class RPCWebServiceRenderer extends WebServiceRenderer
{
    // The key under which we store a user-specific stub instance in the
    // session (if available)
    private static final String INSTANCE_SESSION_ATTRIBUTE = "RWSR_INSTANCE";

    private String mClassName;
    private Class mObjectClass;
    private String mRenderMethod;
    private Map mRenderMethods;

    private static Map sTypeHandlers;

    // Initialize type casting meta data
    static
    {
        sTypeHandlers = new HashMap(10);
        sTypeHandlers.put(String.class,
                          new TypeHandler ()
                              {
                                  public Object castFromString(String value)
                                  {
                                      return value;
                                  }
                              });
        TypeHandler handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Boolean.valueOf(value);
                }
            };
        sTypeHandlers.put(boolean.class, handler);
        sTypeHandlers.put(Boolean.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Byte.valueOf(value);
                }
            };
        sTypeHandlers.put(byte.class, handler);
        sTypeHandlers.put(Byte.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Short.valueOf(value);
                }
            };
        sTypeHandlers.put(short.class, handler);
        sTypeHandlers.put(Short.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Integer.valueOf(value);
                }
            };
        sTypeHandlers.put(int.class, handler);
        sTypeHandlers.put(Integer.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Long.valueOf(value);
                }
            };
        sTypeHandlers.put(long.class, handler);
        sTypeHandlers.put(Long.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Float.valueOf(value);
                }
            };
        sTypeHandlers.put(float.class, handler);
        sTypeHandlers.put(Float.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return Double.valueOf(value);
                }
            };
        sTypeHandlers.put(double.class, handler);
        sTypeHandlers.put(Double.class, handler);

        handler = new TypeHandler()
            {
                public Object castFromString(String value)
                {
                    if (value == null)
                    {
                        return null;
                    }
                    return new BigDecimal(value);
                }
            };
        sTypeHandlers.put(BigDecimal.class, handler);
    }

    /**
     * Performs the extra steps required to initialize and validate an
     * <code>RPCWebServiceRenderer</code> once its properties have been set
     * with the appropriate accessor methods.
     * @exception NodeHandlerException if not all properties have been set
     * correctly
     */
    public void postInitialize()
        throws NodeHandlerException
    {
        super.postInitialize();
        if (mClassName == null)
        {
            throw new NodeHandlerException("className not set");
        }
        if (mRenderMethod == null)
        {
            throw new NodeHandlerException("renderMethod not set");
        }
        try
        {
            // Load the class. We have to use the the current thread's 'context
            // class loader'(i.e. the web app's class loader) to load the
            // class, so that it can be picked up from within the web app
            mObjectClass = Class.forName(
                mClassName,
                true,
                Thread.currentThread().getContextClassLoader());

            // Put the methods with matching name into a hash table, indexed by parameter list length
            mRenderMethods = new HashMap();
            Method[] methods = mObjectClass.getMethods();
            for (int i=0; i<methods.length; i++)
            {
                if (methods[i].getName().equals(mRenderMethod))
                {
                    mRenderMethods.put(new Integer(methods[i].getParameterTypes().length),
                                       methods[i]);
                }
            }
        }
        catch (RuntimeException re)
        {
            throw re;
        }
        catch (Exception e)
        {
            throw new NodeHandlerException(e);
        }

        // Make sure the method name is valid
        if (mRenderMethods.size() == 0)
        {
            throw new NodeHandlerException(new SimpleStringBuffer(100)
                .append("No such method \"")
                .append(mRenderMethod)
                .append("\"").toString());
        }
    }

    /**
     * Sets the fully-qualified class name of the web service Java stub.
     *
     * @param className fully-qualified class name.
     */
    public void setClassName(String className)
    {
        // Make sure we are not in read-only 'locked down' state
        checkLocked("setClassName");

        mClassName = className;
    }

    /**
     * Sets the name of the method to be called on an instance of the web
     * service stub to render a request.
     *
     * @param methodName a method name.
     */
    public void setRenderMethod(String methodName)
    {
        // Make sure we are not in read-only 'locked down' state
        checkLocked("setRenderMethod");

        mRenderMethod = methodName;
    }

    /**
     * Defines a parameter to be passed to the render method. The parameter
     * value will be converted to the appropriate type, according to the
     * argument list of the method.
     * @param param object representing a parameter to be passed to the render
     * method. Should be a subclass of {@link
     * WebServiceRenderer.ParameterBinding}.
     * @exception PortletException if the given object is not a valid {@link
     * WebServiceRenderer.ParameterBinding}
     */
    public void addParam(Object param)
        throws PortletException
    {
        // Make sure we are not in read-only 'locked down' state
        checkLocked("addParam");

        if (!(param instanceof ParameterBinding))
        {
            throw new PortletException(new SimpleStringBuffer(100)
                .append("param object must be of class ")
                .append(ParameterBinding.class.getName()).toString());
        }
        addBinding((ParameterBinding)param);
    }

    /**
     * Defines a constant parameter to be passed to the render method. The
     * parameter value will be converted to the appropriate type, according to
     * the argument list of the method. This is provided as a shorthand for
     * calling {@link #addParam(Object)} with an instance of {@link
     * WebServiceRenderer.ConstParam}.
     * @param param constant value to be passed as an argument to the render
     * method.
     */
    public void addParam(String param)
    {
        // Make sure we are not in read-only 'locked down' state
        checkLocked("addParam");

        addBinding(new ConstParam(param));
    }

    /**
     * Determines the values of parameter bindings using the given
     * <code>WebServiceRenderRequest</code>, invokes the method on the web
     * service stub, and adds an XML representation of its return value to the
     * response XML document.
     * @param wrr an object exposing the relevant properties of the current
     * render request
     * @param docEl the document element of the XML document to which an XML
     * representation of the response should be added
     * @exception PortletException if an error occurs.
     */
    public void invokeService (WebServiceRenderRequest wrr, Element docEl)
        throws PortletException
    {
        // Get the document to use as a factory for elements to be added to the
        // response
        Document responseDoc = docEl.getOwnerDocument();

        Element responseEl = responseDoc.createElement("response");
        docEl.appendChild(responseEl);

        LiteralXML literalXML = getLiteral();

        Method renderMethod;
        Object[] parameters = null;
        if (literalXML == null)
        {
            // Get our parameter bindings
            ParameterBinding[] bindings = getBindings();
            int length = bindings == null ? 0 : bindings.length;

            renderMethod = (Method)mRenderMethods.get(new Integer(length));
            if (renderMethod == null)
            {
                throw new PortletException(new SimpleStringBuffer(100)
                    .append("No such method \"")
                    .append(mRenderMethod)
                    .append("\" taking ")
                    .append(length)
                    .append(" parameters").toString());
            }

            // Convert each argument element to the appropriate type
            Class[] parameterTypes = renderMethod.getParameterTypes();
            parameters = new Object[parameterTypes.length];

            for (int i=0; i<parameterTypes.length; i++)
            {
                String elementVal = bindings[i].getValue(wrr);

                // Load the parameter value into the parameter list
                if (elementVal != null)
                {
                    TypeHandler th = (TypeHandler)sTypeHandlers.get(parameterTypes[i]);
                    if (th == null)
                    {
                        throw new PortletException(new SimpleStringBuffer(100)
                            .append(getClass().getName())
                            .append(" cannot handle parameters of type ")
                            .append(parameterTypes[i].getName()).toString());
                    }
                    parameters[i] = th.castFromString(elementVal);
                }
            }
        } // end if literalXML == null
        // Handle literal XML arguments
        else
        {
            NodeList nl = literalXML.generateElements(wrr);
            int length = nl == null ? 0 : nl.getLength();

            renderMethod = (Method)mRenderMethods.get(new Integer(length));

            if (renderMethod != null)
            {            
                Class[] parameterTypes = renderMethod.getParameterTypes();
                // Convert each argument element to the appropriate type
                parameters = new Object[parameterTypes.length];
                for (int i=0; i<parameterTypes.length; i++)
                {
                    if (parameterTypes[i] != Element.class)
                    {
                        renderMethod = null;
                        break;
                    }
                    parameters[i] = nl.item(i);
                }
            }

            if (renderMethod == null)
            {
                throw new PortletException(new SimpleStringBuffer(100)
                    .append("No such method \"")
                    .append(mRenderMethod)
                    .append("\" taking ")
                    .append(length)
                    .append(" Element parameters").toString());
            }
        }

        // Invoke the method and get the result
        Object result;
        try
        {
            // Get an instance of the stub
            Object instance;

            // If we don't have a session, we simply create a new stub instance
            // every time, for thread safety
            ProviderSession session = wrr.getSession();
            if (session == null)
            {
                instance = mObjectClass.newInstance();
                result = renderMethod.invoke(instance, parameters);
            }
            // If we have a session, we try to re-use a stub for this user
            // stored in the session, thus allowing limited connection pooling
            // and 'stateful' web services. Unfortunately, this requires
            // synchronization.
            else 
            {

                // Find or create the stub for this user in the session
                String attribName = new SimpleStringBuffer(100)
                    .append(wrr.getParameterPrefix())
                    .append(INSTANCE_SESSION_ATTRIBUTE).toString();
                synchronized (session)
                {
                    instance = session.getAttribute(attribName);
                    if (instance == null)
                    {
                        instance = mObjectClass.newInstance();
                        session.setAttribute(attribName, instance);
                    }
                }

                // Because the stubs are generally not thread safe, we can only
                // invoke one method at a time on the same stub
                synchronized (instance)
                {
                    result = renderMethod.invoke(instance, parameters);
                }

            } // end if we have a session
        }
        catch (InvocationTargetException ite)
        {
            throw new PortletException(ite.getTargetException());
        }
        catch (RuntimeException re)
        {
            throw re;
        }
        catch (Exception e)
        {
            throw new PortletException(e);
        }

        // Convert the result to XML
        Element val = responseDoc.createElement("return");
        responseEl.appendChild(val);
        expandResult(responseDoc, val, result);
    }

    // Convert a given object to XML, recursively exanding out arrays
    protected void expandResult (Document doc, Element el, Object result)
    {
        if (result == null)
        {
            return;
        }
        if (result.getClass().isArray())
        {
            int length = Array.getLength(result);
            // Recursively expand each element in the array
            for (int i=0; i<length; i++)
            {
                Element itemEl = doc.createElement("item");
                el.appendChild(itemEl);
                expandResult(doc, itemEl, Array.get(result, i));
            }
        }
        // Support literal XML return values
        else if (result instanceof Element)
        {
            Node resultEl = doc.importNode((Element)result, true);
            el.appendChild(resultEl);
        }
        else
        {
            Text value = doc.createTextNode(result.toString());
            el.appendChild(value);
        }
    }

    private static abstract class TypeHandler
    {
        public abstract Object castFromString(String value);
    }
}
