/*
 *
 * 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.qpid.management.wsdm.muse.resources;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.namespace.QName;

import org.apache.muse.core.Capability;
import org.apache.muse.core.Environment;
import org.apache.muse.core.ResourceManager;
import org.apache.muse.core.routing.MessageHandler;
import org.apache.muse.util.xml.XmlUtils;
import org.apache.muse.ws.addressing.EndpointReference;
import org.apache.muse.ws.addressing.WsaConstants;
import org.apache.muse.ws.addressing.soap.SoapConstants;
import org.apache.muse.ws.addressing.soap.SoapFault;
import org.apache.muse.ws.addressing.soap.SoapUtils;
import org.apache.muse.ws.resource.WsResource;
import org.apache.muse.ws.resource.metadata.MetadataDescriptor;
import org.apache.muse.ws.resource.metadata.WsrmdConstants;
import org.apache.muse.ws.resource.metadata.impl.SimpleMetadataDescriptor;
import org.apache.muse.ws.resource.metadata.impl.WsrmdUtils;
import org.apache.muse.ws.resource.properties.ResourcePropertyCollection;
import org.apache.muse.ws.resource.properties.impl.SimpleResourcePropertyCollection;
import org.apache.muse.ws.resource.properties.impl.WsrpUtils;
import org.apache.muse.ws.resource.properties.schema.ResourcePropertiesSchema;
import org.apache.muse.ws.resource.properties.schema.impl.SimpleResourcePropertiesSchema;
import org.apache.muse.ws.wsdl.WsdlUtils;
import org.apache.qpid.management.Messages;
import org.apache.qpid.management.Names;
import org.apache.qpid.management.wsdm.common.ThreadSessionManager;
import org.apache.qpid.transport.util.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * QMan WS resource.
 * This is the WS Resource wrapper of a QMan managed entity.
 * 
 * @author Andrea Gazzarini
 * TODO :Refactoring :: use STATE Pattern!
 */
@SuppressWarnings("unchecked")
public class QManWsResource implements WsResource
{    
	private final static Logger LOGGER = Logger.get(QManWsResource.class);
	
	// Utility class for logging.
	private final static class Log {
		static void debugElement(String message,Element element) 
		{
			if (LOGGER.isDebugEnabled())
			{
				LOGGER.debug(message, XmlUtils.toString(element));
			}
		}
	}
	
    private Map<String,Capability>  _capabilitiesByAction = new HashMap<String, Capability>();
    private Map<String, Capability> _capabilitiesByURI = new LinkedHashMap<String, Capability>();
    
    private String _contextPath;
    private Environment _environment;    
    private EndpointReference _enpointReference;
    
    private boolean _hasBeenInitialized;
    private boolean _hasBeenShutdown;
    
    private ResourceManager _resourceManager;
    private ResourcePropertyCollection _properties;

    private Map<String,String> _initParameters = Collections.EMPTY_MAP;
    
    private java.util.logging.Logger _logger;
    
    private Document _wsdl;    
    private String _wsdlPath;
    private QName _wsdlPortType;
    
    /**
     * Adds the given capability to this resource.
     * 
     * @param capability the capability to be added.
     */
    public void addCapability(Capability capability)
    {
        capability.setResource(this);
        capability.setLog(getLog());
        capability.setEnvironment(getEnvironment());
        
        String uri = capability.getCapabilityURI();
        _capabilitiesByURI.put(uri, capability);
        
        LOGGER.debug(Messages.QMAN_200033_CAPABILITY_CLASS_HAS_BEEN_ADDED, capability.getClass(),uri);
    }
    
    /**
     * Returns the capability associated with the given URI.
     * 
     * @return the capability associated with the given URI.
     */
    public final Capability getCapability(String capabilityURI)
    {
        return _capabilitiesByURI.get(capabilityURI);
    }
    
    /**
     * Returns all the WS-Action URIs supported by this resource.
     * 
     * @return all of the WS-A Action URIs supported by this resource.
     */
    protected Collection getCapabilityActions()
    {
        return Collections.unmodifiableSet(_capabilitiesByAction.keySet());
    }
    
    /**
     * Returns the capability associated with the given action.
     * 
     * @param action the wsa:action of the requested capability.
     * @return the capability associated with the given action.
     */
    protected Capability getCapabilityForAction(String action)
    {
        return (Capability)_capabilitiesByAction.get(action);
    }
    
    /**
     * Returns a collection with all registered capability URIs.
     * 
     * @return a collection with all registered capability URIs.
     */
    public final Collection getCapabilityURIs()
    {
        return Collections.unmodifiableSet(_capabilitiesByURI.keySet());
    }
    
    /**
     * Returns the context path of this resource.
     * 
     * @return the context path of this resource.
     */
    public final String getContextPath()
    {
        return _contextPath;
    }

    /**
     * Returns the endpoint reference of this resource.
     * 
     * @return the endpoint reference of this resource.
     */
    public EndpointReference getEndpointReference()
    {
        return _enpointReference;
    }
    
    /**
     * Returns the enviroment associated with this resource.
     * 
     * @return the enviroment associated with this resource.
     */
    public final Environment getEnvironment()
    {
        return _environment;
    }
    
    /**
     * Returns the initialization parameter of this resource associated with 
     * the given name.
     * 
     * @param name the init parameter name.
     * @return the initialization parameter associated with the given name.
     */
    public final String getInitializationParameter(String name)
    {
        return (String)getInitializationParameters().get(name);
    }
    
    /**
     * Returns the map containing all init parameters of this resource.
     * 
     * @return the map containing all init parameters of this resource.
     */
    public final Map<String,String> getInitializationParameters()
    {
        return _initParameters;
    }
    
    /**
     * N.A. This resource uses QMan logging instead of plain java.util.logger 
     * implementation.
     */
    public final java.util.logging.Logger getLog()
    {
        return _logger;
    }
    
    /**
     * Returns the resource manager associated with this resource.
     * 
     * @return the resource manager associated with this resource.
     */
    public ResourceManager getResourceManager()
    {
        return _resourceManager;
    }
    
    /**
     * Returns the wsdl (relative) path of this resource.
     * 
     * @return the wsdl (relative) path of this resource.
     */
    public String getWsdlPath()
    {
        return _wsdlPath;
    }
    
    /**
     * Returns the port type of this resource.
     * 
     * @return the port type of this resource.
     */
    public final QName getWsdlPortType()
    {
        return _wsdlPortType;
    }
    
    /**
     * Returns true if this resource has been initialized, false otherwise.
     * 
     * @return true if this resource has been initialized, false otherwise.
     */
    public final boolean hasBeenInitialized()
    {
        return _hasBeenInitialized;
    }
    
    /**
     * Returns true if this resource has been shutdown, false otherwise.
     * 
     * @return true if this resource has been shutdown, false otherwise.
     */
    public final boolean hasBeenShutdown()
    {
        return _hasBeenShutdown;
    }
    
    /**
     * Checks if a capability with the given URI is available for this resource.
     * 
     * @return true if a capability with the given URI is available for this resource, false otherwise.
     */
    public final boolean hasCapability(String capabilityURI)
    {
        return getCapability(capabilityURI) != null;
    }
    
    /**
     * Creates a metadata descriptor for this resource.
     * 
     * @param wsdl the WSDL document. 
     * @return a metadata descriptor for this resource.
     * @throws SoapFault when it's not possible build the descriptor.
     */
    protected MetadataDescriptor createMetadataDescriptor(Document wsdl) throws SoapFault
    {
        try 
        {    	
	        Element portTypeXML = WsdlUtils.getPortType(wsdl, getWsdlPortType());
	        
	        String rmdName = XmlUtils.getAttribute(portTypeXML, WsrmdConstants.DESCRIPTOR_ATTR_QNAME);
	        String rmdPath = XmlUtils.getAttribute(portTypeXML, WsrmdConstants.DESCRIPTOR_LOCATION_ATTR_QNAME);
	        
	        LOGGER.debug(Messages.QMAN_200034_RMD_NAME, rmdName);
	        LOGGER.debug(Messages.QMAN_200035_RMD_PATH, rmdPath);
	        
	        Environment env = getEnvironment();
	        String path = env.createRelativePath(getWsdlPath(), rmdPath);        
	        Document rmdDoc = env.getDocument(path);
	        
	        Element[] additionalProperties = ThreadSessionManager.getInstance().getSession().getResourceMetadataDescriptor();
	        Element metadataDescriptor = WsrmdUtils.getMetadataDescriptor(rmdDoc, rmdName);
        
	        for (Element element : additionalProperties) 
	        {
				rmdDoc.adoptNode(element);
				metadataDescriptor.appendChild(element);
				
				Log.debugElement(Messages.QMAN_200036_ADDITIONAL_RMD_PROPERTY,element);
			}
			
			return new SimpleMetadataDescriptor(metadataDescriptor);
        } 
        catch(Exception exception)
        {
        	LOGGER.error(exception,Messages.QMAN_100021_RMD_BUID_FAILURE,getContextPath());
        	throw new SoapFault(exception);
        }
    }

    /**
     * Creates a WSRP document representing schema properties for this resource.
     * 
     * @param wsdl the DOM document holding the resource's WSDL.       
     * @return the WSRP document schema.
     */
    protected ResourcePropertiesSchema createPropertiesSchema(Document wsdl)
    {
        QName wsrpName = WsrpUtils.getPropertiesName(wsdl, getWsdlPortType());
        Element wsrpDoc = WsdlUtils.getElementDeclaration(wsdl, wsrpName);        
        return new SimpleResourcePropertiesSchema(wsrpName, wsrpDoc);
    }    
    
    /**
     * Returns the collection containing all properties of this resource.
     * 
     * @return the collection containing all properties of this resource.
     */
    public final ResourcePropertyCollection getPropertyCollection()
    {
        return _properties;
    }    
    
    /**
     * Return the WSDL document of this resource.
     * 
     * @return the WSDL document of this resource.
     */
    public Document getWsdl()
    {
    	return _wsdl;
    }
    
    /**
     * Initializes this resources.
     * 
     * @throws SoapFault when the initialization fails.
     */
    public void initialize() throws SoapFault
    {    	
        _properties = new SimpleResourcePropertyCollection();
        _wsdl = ThreadSessionManager.getInstance().getSession().getWsdlDocument();
        
        ResourcePropertiesSchema schema = createPropertiesSchema(_wsdl);
        _properties.setSchema(schema);
      
        MetadataDescriptor metadata = createMetadataDescriptor(_wsdl);
        _properties.setMetadata(metadata);
                
        initializeCapabilities();
        
        _hasBeenInitialized = true;
        
        _properties.applyMetadata();
        
        if (Boolean.parseBoolean(getInitializationParameter(Names.VALIDATE_WSRP_PARAM)))
	    {
        	_properties.validateSchema();	        	
	    }

        _properties.validateMetadata();
    }
    
    /**
     * Initializes capabilities of this resource.
     * 
     * @throws SoapFault when at least one capability fails to initialize.
     */
    public void initializeCapabilities() throws SoapFault
    {
        for (Entry<String, Capability> entry : _capabilitiesByURI.entrySet()) 
        {
        	Capability capability = entry.getValue();
			capability.initialize();
			
			for (Object action : capability.getActions()) 
			{
                _capabilitiesByAction.put((String)action, capability);
			}
			
			capability.initializeCompleted();
		}
    }
    
    /**
     * Invokes the action specified in the given soap request on this resource.
     * 
     * @param requestBody the SOAP body.
     * @return the result of the invocation as org.w3c.dom.Element
     */
    public Element invoke(Element requestBody)
    {
        String action =  _environment.getAddressingContext().getAction();  
        Capability capability = getCapabilityForAction(action);
        
        // Sanity check : is there a capability for the given action?
        if (capability == null)
        {
            SoapFault wsaFault = new SoapFault(String.format(Messages.ACTION_NOT_SUPPORTED, action,getContextPath()));

            wsaFault.setCode(SoapConstants.SENDER_QNAME);
            wsaFault.setSubCode(WsaConstants.ACTION_NOT_SUPPORTED_FAULT_QNAME);
            
            Element detail = XmlUtils.createElement(WsaConstants.PROBLEM_ACTION_QNAME);
            XmlUtils.setElement(detail, WsaConstants.ACTION_QNAME, action);
            wsaFault.setDetail(detail);
            
            LOGGER.error(Messages.QMAN_100020_ACTION_NOT_SUPPORTED, action,getContextPath());

            return wsaFault.toXML();
        }
        
        MessageHandler handler = capability.getMessageHandler(action);
        Method method = handler.getMethod();
        
        try
        {
        	Object[]parameters = handler.fromXML(requestBody);
            Object result = method.invoke(capability, parameters);
            return handler.toXML(result);
        }
        catch (Throwable throwable)
        {
            SoapFault response = SoapUtils.convertToFault(
            		(throwable.getCause()!= null) 
            			? throwable.getCause()
            			: throwable);
            return response.toXML();
        }
    }
    
    /**
     * Sets the context path of this resource.
     * 
     * @param contextPath the context path of this resource.
     */
    public final void setContextPath(String contextPath)
    {
        _contextPath = contextPath;
    }
    
    /**
     * Sets the endpoint reference of this resource.
     * 
     * @param endpointReference the endpoint reference of this resource.
     */
    public final void setEndpointReference(EndpointReference endpointReference)
    {
        if (_enpointReference != null && hasBeenInitialized())
            throw new RuntimeException(("ExistingResourceEPR"));
        
        _enpointReference = endpointReference;
    }
    
    /**
     * Sets the context environment of this resource.
     * 
     * @param environment the context environment of this resource.
     */
    public final void setEnvironment(Environment environment)
    {
        _environment = environment;
    }
    
    /**
     * Sets the initialization parameters of this resource.
     * 
     * @param parameters the init parameters of this resource.
     */
    public final void setInitializationParameters(Map parameters)
    {
        _initParameters = (parameters != null) 
        	? parameters 
        	: Collections.EMPTY_MAP;
    }
    
    /**
     * N.A. for this resource. QMan logging mechanism is used for that.
     */
    public final void setLog(java.util.logging.Logger log)
    {
        _logger = log;
    }
    
    /**
     * Sets the resource manager owner of this resource.
     * 
     * @param manager the resource manager of this resource.
     */
    public void setResourceManager(ResourceManager manager)
    {
        _resourceManager = manager;
    }
    
    /**
     * Sets the WSDL (relative) path of this resource.
     * 
     * @param wsdlPath the WSDL (relative) path of this resource.
     */
    public final void setWsdlPath(String wsdlPath)
    {
    	this._wsdlPath = wsdlPath;
    }
    
    /**
     * Sets the port type of this resource.
     * 
     * @param wsdlPortType the port type of this resource.
     */
    public final void setWsdlPortType(QName wsdlPortType)
    {
        _wsdlPortType = wsdlPortType;
    }
        
    /**
     * Shutdown procedure for this resource.
     * 
     * @throws SoapFault when the shutdown procedure fails.
     */
    public synchronized void shutdown() throws SoapFault
    {
        if (hasBeenShutdown())
            throw new SoapFault(("ResourceAlreadyDestroyed"));
        
        if (!hasBeenInitialized())
            throw new SoapFault(("ResourceNotInitialized"));

        _hasBeenShutdown = true;
        
        shutdownCapabilities();
                
        ResourceManager manager = getResourceManager();

        //
        // remove resource visibility
        // 
        if (manager.getResource(_enpointReference) != null)
            manager.removeResource(_enpointReference);
    }
    
    /**
     * Shutdown procedure for all registered capabilities of this resource.
     * 
     * @throws SoapFault when at least one capability shutdown fails.
     */
    protected void shutdownCapabilities() throws SoapFault
    {
        for (Entry<String,Capability> entry : _capabilitiesByURI.entrySet()) 
        {
        	Capability capabilty = entry.getValue();
        	capabilty.prepareShutdown();
        	capabilty.shutdown();
		}        
    }
    
    /**
     * Returns a string representation of this resource.
     * Basically the resource endpoint reference (as a string) is returned.
     * 
     * @return the resource endpoint reference (as a string) is returned.
     */
    public String toString()
    {
        return getEndpointReference().toString();
    }     
}