/*
 * 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.context.externalize;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.axis2.description.AxisMessage;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.AxisServiceGroup;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.engine.Handler;
import org.apache.axis2.transport.TransportListener;
import org.apache.axis2.util.MetaDataEntry;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * Some Externalize objects must be "activated" after they are read.
 * Activation normally involves associating the object with objects in the current
 * runtime.
 * 
 * ActivateUtils provides activation related utilities
 */
public class ActivateUtils {
    
    private static final Log log = LogFactory.getLog(ActivateUtils.class);
    
    /**
     * Private Constructor
     * All methods in ActivateUtils are static.
     */
    private ActivateUtils() {}

    /**
     * Find the AxisServiceGroup object that matches the criteria
     * <p/>
     * <B>Note<B> the saved service group meta information may not
     * match up with any of the serviceGroups that
     * are in the current AxisConfiguration object.
     * 
     * @param axisConfig The AxisConfiguration object
     * @param serviceGrpClassName the class name string for the target object
     *                   (could be a derived class)
     * @param serviceGrpName      the name associated with the service group
     * @return the AxisServiceGroup object that matches the criteria
     */
    public static AxisServiceGroup findServiceGroup(AxisConfiguration axisConfig,
                                                    String serviceGrpClassName,
                                                    String serviceGrpName) {
        Iterator its = axisConfig.getServiceGroups();

        while (its.hasNext()) {
            AxisServiceGroup serviceGroup = (AxisServiceGroup) its.next();

            String tmpSGClassName = serviceGroup.getClass().getName();
            String tmpSGName = serviceGroup.getServiceGroupName();

            if (tmpSGClassName.equals(serviceGrpClassName)) {
                boolean found = false;

                // the serviceGroupName can be null, so either both the 
                // service group names are null or they match
                if ((tmpSGName == null) && (serviceGrpName == null)) {
                    found = true;
                } else if ((tmpSGName != null) && (tmpSGName.equals(serviceGrpName))) {
                    found = true;
                }

                if (found) {
                    // trace point
                    if (log.isTraceEnabled()) {
                        log.trace("ObjectStateUtils:findServiceGroup(): returning  ["
                                + serviceGrpClassName + "]   [" + serviceGrpName + "]");
                    }

                    return serviceGroup;
                }
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findServiceGroup(): [" + serviceGrpClassName + "]   ["
                    + serviceGrpName + "]  returning  [null]");
        }

        return null;
    }
    
    /**
     * Find the AxisService object that matches the criteria
     * 
     * @param axisConfig The AxisConfiguration object
     * @param serviceClassName the class name string for the target object
     *                   (could be a derived class)
     * @param serviceName      the name associated with the service
     * @return the AxisService object that matches the criteria
     */
    public static AxisService findService(AxisConfiguration axisConfig, String serviceClassName,
                                          String serviceName) {
        HashMap services = axisConfig.getServices();

        Iterator its = services.values().iterator();

        while (its.hasNext()) {
            AxisService service = (AxisService) its.next();

            String tmpServClassName = service.getClass().getName();
            String tmpServName = service.getName();

            if ((tmpServClassName.equals(serviceClassName)) && (tmpServName.equals(serviceName))) {
                // trace point
                if (log.isTraceEnabled()) {
                    log.trace("ObjectStateUtils:findService(): returning  [" + serviceClassName
                            + "]   [" + serviceName + "]");
                }

                return service;
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findService(): [" + serviceClassName + "]   ["
                    + serviceName + "]  returning  [null]");
        }

        return null;
    }
    
    /**
     * Find the AxisOperation object that matches the criteria
     * 
     * @param axisConfig The AxisConfiguration object
     * @param opClassName the class name string for the target object
     *                   (could be a derived class)
     * @param opQName    the name associated with the operation
     * @return the AxisOperation object that matches the given criteria
     */
    public static AxisOperation findOperation(AxisConfiguration axisConfig, String opClassName,
                                              QName opQName) {
        HashMap services = axisConfig.getServices();

        Iterator its = services.values().iterator();

        while (its.hasNext()) {
            AxisService service = (AxisService) its.next();

            Iterator ito = service.getOperations();

            while (ito.hasNext()) {
                AxisOperation operation = (AxisOperation) ito.next();

                String tmpOpName = operation.getClass().getName();
                QName tmpOpQName = operation.getName();

                if ((tmpOpName.equals(opClassName)) && (tmpOpQName.equals(opQName))) {
                    // trace point
                    if (log.isTraceEnabled()) {
                        log.trace("ObjectStateUtils:findOperation(axisCfg): returning  ["
                                + opClassName + "]   [" + opQName.toString() + "]");
                    }

                    return operation;
                }
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findOperation(axisCfg): [" + opClassName + "]   ["
                    + opQName.toString() + "]  returning  [null]");
        }

        return null;
    }


    /**
     * Find the AxisOperation object that matches the criteria
     * 
     * @param service    The AxisService object
     * @param opClassName The class name string for the target object
     *                   (could be a derived class)
     * @param opQName    the name associated with the operation
     * @return the AxisOperation object that matches the given criteria
     */
    public static AxisOperation findOperation(AxisService service, String opClassName,
                                              QName opQName) {
        if (service == null) {
            return null;
        }

        Iterator ito = service.getOperations();

        // Previous versions of Axis2 didn't use a namespace on the operation name, so they wouldn't
        // have externalized a namespace.  If that's the case, only compare the localPart of the
        // operation name
        String namespace = opQName.getNamespaceURI();
        boolean ignoreNamespace = false;
        if (namespace == null || "".equals(namespace)) {
            ignoreNamespace = true;
        }
        
        while (ito.hasNext()) {
            AxisOperation operation = (AxisOperation) ito.next();

            String tmpOpName = operation.getClass().getName();
            QName tmpOpQName = operation.getName();
            
            if ((tmpOpName.equals(opClassName)) && 
                ((ignoreNamespace && (tmpOpQName.getLocalPart().equals(opQName.getLocalPart())) || (tmpOpQName.equals(opQName))))) {

                if (log.isTraceEnabled()) {
                    log.trace("ObjectStateUtils:findOperation(service): ignoreNamespace [" + ignoreNamespace
                    		+ "] returning  ["
                            + opClassName + "]   [" + opQName.toString() + "]");
                }

                return operation;
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findOperation(service): ignoreNamespace [" + ignoreNamespace
                    + " classname [" + opClassName + "]  QName ["
                    + opQName.toString() + "]  returning  [null]");
        }

        return null;
    }

    /**
     * Find the AxisMessage object that matches the criteria
     * 
     * @param op             The AxisOperation object
     * @param msgName        The name associated with the message
     * @param msgElementName The name associated with the message element
     * @return the AxisMessage object that matches the given criteria
     */
    public static AxisMessage findMessage(AxisOperation op, String msgName, String msgElementName) {
        // Several kinds of AxisMessages can be associated with a particular 
        // AxisOperation.  The kinds of AxisMessages that are typically
        // accessible are associated with "in" and "out".  
        // There are also different kinds of AxisOperations, and each
        // type of AxisOperation can have its own mix of AxisMessages
        // depending on the style of message exchange pattern (mep)

        if (op == null) {
            // trace point
            if (log.isTraceEnabled()) {
                log.trace("ObjectStateUtils:findMessage(): [" + msgName + "]  [" + msgElementName
                        + "] returning  [null] - no AxisOperation");
            }

            return null;
        }

        if (msgName == null) {
            // nothing to match with, expect to match against a name
            // trace point
            if (log.isTraceEnabled()) {
                log.trace("ObjectStateUtils:findMessage(): [" + msgName + "]  [" + msgElementName
                        + "] returning  [null] - message name is not set");
            }

            return null;
        }


        String tmpName = null;
        String tmpElementName = null;

        //-------------------------------------
        // first try the "out" message
        //-------------------------------------
        AxisMessage out = null;
        try {
            out = op.getMessage(WSDLConstants.MESSAGE_LABEL_OUT_VALUE);
        } catch (Exception ex) {
            // just absorb the exception
        }

        if (out != null) {
            tmpName = out.getName();

            QName tmpQout = out.getElementQName();
            if (tmpQout != null) {
                tmpElementName = tmpQout.toString();
            }
        }

        // check the criteria for a match

        boolean matching = matchMessageNames(tmpName, tmpElementName, msgName, msgElementName);

        if (matching) {
            // trace point
            if (log.isTraceEnabled()) {
                log.trace("ObjectStateUtils:findMessage(): returning OUT message  [" + msgName
                        + "]  [" + msgElementName + "] ");
            }

            return out;
        }

        //-------------------------------------
        // next, try the "in" message 
        //-------------------------------------
        AxisMessage in = null;
        try {
            in = op.getMessage(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
        } catch (Exception ex) {
            // just absorb the exception
        }

        if (in != null) {
            tmpName = in.getName();

            QName tmpQin = in.getElementQName();
            if (tmpQin != null) {
                tmpElementName = tmpQin.toString();
            }
        } else {
            tmpName = null;
            tmpElementName = null;
        }

        // check the criteria for a match

        matching = matchMessageNames(tmpName, tmpElementName, msgName, msgElementName);

        if (matching) {
            // trace point
            if (log.isTraceEnabled()) {
                log.trace("ObjectStateUtils:findMessage(): returning IN message [" + msgName
                        + "]  [" + msgElementName + "] ");
            }

            return in;
        }

        // if we got here, then no match was found

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findMessage(): [" + msgName + "]  [" + msgElementName
                    + "] returning  [null]");
        }

        return null;
    }
    
    /**
     * Find the Handler object that matches the criteria
     * 
     * @param existingHandlers The list of existing handlers and phases
     * @param handlerClassName the class name string for the target object
     *                   (could be a derived class)
     * @return the Handler object that matches the criteria
     */
    public static Object findHandler(List<Handler> existingHandlers, MetaDataEntry metaDataEntry) //String handlerClassName)
    {

        String title = "ObjectStateUtils:findHandler(): ";

        String handlerClassName = metaDataEntry.getClassName();
        String qNameAsString = metaDataEntry.getQNameAsString();

        for (int i = 0; i < existingHandlers.size(); i++) {
            if (existingHandlers.get(i) != null) {
                String tmpClassName = existingHandlers.get(i).getClass().getName();
                String tmpName = ((Handler) existingHandlers.get(i)).getName().toString();

                if ((tmpClassName.equals(handlerClassName)) && (tmpName.equals(qNameAsString))) {
                    // trace point
                    if (log.isTraceEnabled()) {
                        log.trace(title + " [" + handlerClassName + "]  name [" + qNameAsString
                                + "]  returned");
                    }

                    return (Handler) (existingHandlers.get(i));
                }
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace(title + " [" + handlerClassName + "]  name [" + qNameAsString
                    + "] was not found in the existingHandlers list");
        }

        return null;
    }
    
    /**
     * Find the TransportListener object that matches the criteria
     * <p/>
     * <B>Note<B> the saved meta information may not
     * match up with any of the objects that
     * are in the current AxisConfiguration object.
     * 
     * @param axisConfig The AxisConfiguration object
     * @param listenerClassName the class name string for the target object
     *                   (could be a derived class)
     * @return the TransportListener object that matches the criteria
     */
    public static TransportListener findTransportListener(AxisConfiguration axisConfig,
                                                          String listenerClassName) {
        // TODO: investigate a better technique to match up with a TransportListener

        HashMap transportsIn = axisConfig.getTransportsIn();

        // get a collection of the values in the map
        Collection values = transportsIn.values();

        Iterator i = values.iterator();

        while (i.hasNext()) {
            TransportInDescription ti = (TransportInDescription) i.next();

            TransportListener tl = ti.getReceiver();
            String tlClassName = tl.getClass().getName();

            if (tlClassName.equals(listenerClassName)) {
                // trace point
                if (log.isTraceEnabled()) {
                    log.trace("ObjectStateUtils:findTransportListener():  [" + listenerClassName
                            + "]  returned");
                }

                return tl;
            }
        }

        // trace point
        if (log.isTraceEnabled()) {
            log.trace("ObjectStateUtils:findTransportListener(): returning  [null]");
        }

        return null;
    }

    /**
     * Compares the two collections to see if they are equivalent.
     * 
     * @param a1  The first collection
     * @param a2  The second collection
     * @param strict  Indicates whether strict checking is required.  Strict
     *                checking means that the two collections must have the
     *                same elements in the same order.  Non-strict checking
     *                means that the two collections must have the same 
     *                elements, but the order is not significant.
     * @return TRUE if the two collections are equivalent
     *         FALSE, otherwise
     */
    public static boolean isEquivalent(ArrayList a1, ArrayList a2, boolean strict) {
        if ((a1 != null) && (a2 != null)) {
            // check number of elements in lists
            int size1 = a1.size();
            int size2 = a2.size();

            if (size1 != size2) {
                // trace point
                if (log.isTraceEnabled()) {
                    log.trace("ObjectStateUtils:isEquivalent(ArrayList,ArrayList): FALSE - size mismatch ["
                            + size1 + "] != [" + size2 + "]");
                }
                return false;
            }

            if (strict) {
                // Strict checking
                // The lists must contain the same elements in the same order.
                return (a1.equals(a2));
            } else {
                // Non-strict checking
                // The lists must contain the same elements but the order is not required.
                Iterator i1 = a1.iterator();

                while (i1.hasNext()) {
                    Object obj1 = i1.next();

                    if (!a2.contains(obj1)) {
                        // trace point
                        if (log.isTraceEnabled()) {
                            log.trace("ObjectStateUtils:isEquivalent(ArrayList,ArrayList): FALSE - mismatch with element ["
                                    + obj1.getClass().getName() + "] ");
                        }
                        return false;
                    }
                }

                return true;
            }

        } else if ((a1 == null) && (a2 == null)) {
            return true;
        } else if ((a1 != null) && (a2 == null)) {
            if (a1.size() == 0) {
                return true;
            }
            return false;
        } else if ((a1 == null) && (a2 != null)) {
            if (a2.size() == 0) {
                return true;
            }
            return false;
        } else {
            // mismatch

            // trace point
            if (log.isTraceEnabled()) {
                log.trace("ObjectStateUtils:isEquivalent(ArrayList,ArrayList): FALSE - mismatch in lists");
            }
            return false;
        }
    }

    /**
     * Compares the two collections to see if they are equivalent.
     * 
     * @param m1  The first collection
     * @param m2  The second collection
     * @param strict  Indicates whether strict checking is required.  Strict
     *                checking means that the two collections must have the
     *                same mappings.  Non-strict checking means that the 
     *                two collections must have the same keys.  In both
     *                cases, the order is not significant.
     * @return TRUE if the two collections are equivalent
     *         FALSE, otherwise
     */
    public static boolean isEquivalent(Map m1, Map m2, boolean strict) {
        if ((m1 != null) && (m2 != null)) {
            if (strict) {
                // This is a strict test.
                // Returns true if the given object is also a map and the two Maps 
                // represent the same mappings. 
                return (m1.equals(m2));
            } else {
                int size1 = m1.size();
                int size2 = m2.size();

                if (size1 != size2) {
                    return false;
                }

                // check the keys, ordering is not important between the two maps
                Iterator it1 = m1.keySet().iterator();

                while (it1.hasNext()) {
                    Object key1 = it1.next();

                    if (m2.containsKey(key1) == false) {
                        return false;
                    }
                }

                return true;
            }
        } else if ((m1 == null) && (m2 == null)) {
            return true;
        } else {
            // mismatch
            return false;
        }
    }


    /**
     * Compares the two collections to see if they are equivalent.
     * 
     * @param l1  The first collection
     * @param l2  The second collection
     * @return TRUE if the two collections are equivalent
     *         FALSE, otherwise
     */
    public static boolean isEquivalent(LinkedList l1, LinkedList l2) {
        if ((l1 != null) && (l2 != null)) {
            // This is a strict test.
            // Returns true if the specified object is also a list, 
            // both lists have the same size, and all corresponding pairs 
            // of elements in the two lists are equal where
            // they contain the same elements in the same order.
            return (l1.equals(l2));
        } else if ((l1 == null) && (l2 == null)) {
            return true;
        } else {
            // mismatch
            return false;
        }
    }
    
    /**
     * Check the first set of names for a match against
     * the second set of names.  These names are 
     * associated with AxisMessage objects. Message names
     * are expected to be non-null.  Element names could
     * be either null or non-null.
     * 
     * @param name1  The name for the first message
     * @param elementName1 The element name for the first message
     * @param name2  The name for the second message
     * @param elementName2 The element name for the second message
     * @return TRUE if there's a match,
     *         FALSE otherwise
     */
    private static boolean matchMessageNames(String name1, String elementName1, String name2,
                                             String elementName2) {
        // the name for the message must exist
        if ((name1 != null) && (name2 != null) && (name1.equals(name2))) {
            // there's a match on the name associated with the message object

            // element names need to match, including being null
            if ((elementName1 == null) && (elementName2 == null)) {
                // there's a match for the nulls
                return true;
            } else if ((elementName1 != null) && (elementName2 != null)
                    && (elementName1.equals(elementName2))) {
                // there's a match for the element names
                return true;
            } else {
                // there's some mismatch
                return false;
            }
        } else {
            // either a message name is null or the names don't match
            return false;
        }
    }

}
