/*
 * 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.jaxws.spi.handler;

import org.apache.axis2.java.security.AccessController;
import org.apache.axis2.jaxws.ExceptionFactory;
import org.apache.axis2.jaxws.description.impl.DescriptionUtils;
import org.apache.axis2.jaxws.description.xml.handler.HandlerChainType;
import org.apache.axis2.jaxws.description.xml.handler.HandlerChainsType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;
import java.io.InputStream;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class can be subclassed to produce different implementations of {@link HandlerResolver}
 *
 */
public abstract class BaseHandlerResolver implements HandlerResolver {

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

    // TODO should probably use constants defined elsewhere
    protected static final Map<String, String> protocolBindingsMap =
        new HashMap<String, String>(5);

    protected HandlerChainsType handlerChainsType;
    
    protected BaseHandlerResolver() {
    }
    
    protected BaseHandlerResolver(String file) {
        ClassLoader classLoader = this.getClass().getClassLoader();
        String className = this.getClass().getName();
        InputStream is =
            DescriptionUtils.openHandlerConfigStream(file, className, classLoader);
        
        if(is == null) {
            log.warn("Unable to load handlers from file: " + file);                    
        } else {
            handlerChainsType = DescriptionUtils.loadHandlerChains(is, classLoader);
        }
    }
    
    protected static boolean chainResolvesToPort(HandlerChainType hct, PortInfo portinfo) {
        
        List<String> protocolBindings = hct.getProtocolBindings();
        if (protocolBindings != null) {
            boolean match = true;
            for (Iterator<String> it = protocolBindings.iterator() ; it.hasNext();) {
                match = false;  // default to false in the protocol bindings until we find a match
                String protocolBinding = it.next();
                protocolBinding = protocolBinding.startsWith("##") ? protocolBindingsMap.get(protocolBinding) : protocolBinding;
                // if the protocolBindingsMap returns null, it would mean someone has some nonsense ##binding
                if ((protocolBinding != null) && (protocolBinding.equals(portinfo.getBindingID()))) {
                    match = true;
                    break;
                }
            }
            if (match == false) {
                // we've checked all the protocolBindings, but didn't find a match, no need to continue
                return match;
            }
        }

        /*
         * need to figure out how to get the namespace declaration out of the port-name-pattern and service-name-pattern
         */
        
        if (!doesPatternMatch(portinfo.getPortName(), hct.getPortNamePattern())) {
                // we've checked the port-name-pattern, and didn't find a match, no need to continue
                return false;
        }
        
        if (!doesPatternMatch(portinfo.getServiceName(), hct.getServiceNamePattern())) {
                // we've checked the service-name-pattern, and didn't find a match, no need to continue
                return false;
        }

        return true;
    }
    
    protected static Class loadClass(String clazz) throws ClassNotFoundException {
        try {
            return forName(clazz, true, getContextClassLoader());
        } catch (ClassNotFoundException e) {
            throw e;
        }
    }
    
    /**
     * Returns true of the specified qName matches the pattern.
     * Some customers may have become dependent on the older
     * algorithm.  So first the "official" algorithm is used
     * and if that fails, the older algorithm is used.
     * @param qName QName
     * @param pattern QName
     * @return true or false
     */
    public static boolean doesPatternMatch(QName qName, QName pattern) {
        
        // Validate pattern only generates warnings if the pattern qname
        // does not align with the specification. 
        validatePattern(pattern);
        
        // Try the official pattern match algorithm
        boolean match = doesPatternMatch_Official(qName, pattern);
        
        // Customers may be dependent on the old algorithm, so this is retained.
        if (!match) {
            if (log.isDebugEnabled()) {
                log.debug("The offical matching algorithm failed.  Re-attempting with the prior algorithm");
            }
            match = doesPatternMatch_Old(qName, pattern);
            if (log.isDebugEnabled()) {
                log.debug("The old matching algorithm returns " + match);
            }
        }
        return match;
    }

    /**
     * The old match algorithm combines the namespace and localpart into
     * a single string to do the matching.  Unfortunately this will cause
     * the pure wildcard (*) pattern to fail.  And in addition, it may cause
     * other patterns to succeed.  Prefer the doesPatternMatch_Official algorithm
     * @param portInfoQName
     * @param pattern
     * @return
     */
    private static boolean doesPatternMatch_Old(QName portInfoQName, QName pattern) {
      if (pattern == null)
          return true;
     
      // build up the strings according to the regular expression defined at http://java.sun.com/xml/ns/javaee/javaee_web_services_1_2.xsd
      // use the prefix, not the literal namespace
      String portInfoPrefix = portInfoQName.getNamespaceURI(); //Prefix();
      String portInfoLocalPart = portInfoQName.getLocalPart();
      String portInfoString = ((portInfoPrefix == null) || (portInfoPrefix.equals(""))) ? "" : portInfoPrefix + ":";
      portInfoString += portInfoLocalPart;
      
      String patternStringPrefix = pattern.getNamespaceURI(); //Prefix();
      String patternInfoLocalPart = pattern.getLocalPart();
      String patternString = ((patternStringPrefix == null) || (patternStringPrefix.equals(""))) ? "" : patternStringPrefix + ":";
      patternString += patternInfoLocalPart;
      
      // now match the portInfoQName to the user pattern
      // But first, convert the user pattern to a regular expression.  Remember, the only non-QName character allowed is "*", which
      // is a wildcard, with obvious restrictions on what characters can match (for example, a .java filename cannot contain perentheses).
      // We'll just use part of the above reg ex to form the appropriate restrictions on the user-specified "*" character:
      Pattern userp = Pattern.compile(patternString.replace("*", "(\\w|\\.|-|_)*"));
      Matcher userm = userp.matcher(portInfoString);
      boolean match = userm.matches();
      if (log.isDebugEnabled()) {
          if (!match) {
              log.debug("Pattern match failed: \"" + portInfoString + "\" does not match \"" + patternString + "\"");
          } else {
              log.debug("Pattern match succeeded: \"" + portInfoString + "\" matches \"" + patternString + "\"");
          }
      }
      return match;
      
  }
    
    /**
     * Determine if the indicated qname matches the pattern
     * @param qName
     * @param pattern
     * @return
     */
    private static boolean doesPatternMatch_Official(QName qName, QName pattern) {
        if (log.isDebugEnabled()) {
            log.debug("entry pattern=" + pattern + " qname=" + qName);
        }
        if (pattern == null) {
            if (log.isDebugEnabled()) {
                log.debug("Successful Match: Pattern is null");
            }
            return true;
        }
        
        // Do a pattern match on the local part of the qname.
        String patternLocalPart = pattern.getLocalPart();
        String localPart = qName.getLocalPart();
        
        // Replace the wildcard (*) references in the QName with an appropriate regular expression.
        String regEx = patternLocalPart.replace("*", "(\\w|\\.|-|_)*");
        Pattern userp = Pattern.compile(regEx);
        Matcher userm = userp.matcher(localPart);
        boolean match = userm.matches();
        if (!match) {
            if (log.isDebugEnabled()) {
                log.debug("No Match: The local name does not match the regex pattern: " + regEx);
            }
            return false;
        } 
        
        
        // Now do the matching with the namespace.
        // If the entire pattern is a wildcard (*), then all namespaces are acceptable.
        // For example:
        //     <port-name-pattern>*</port-name-pattern>
        // 
        // In such cases, the assumption is that the pattern namespace will be empty.
        String patternNamespace = pattern.getNamespaceURI();
        String namespace = qName.getNamespaceURI();
        if (patternNamespace.length() == 0) {  // By definition, a namespace will never be null.
            if (log.isDebugEnabled()) {
                log.debug("Successful Match: The local name matches and the pattern namespace is empty.");
            }
            return true;
        }
        
        // If a namespace is specified, it will be done via a prefix.  For example:
        //    <port-name-pattern>p:MyPortName</port-name-pattern>
        // Thus according to the current pattern structure there is no way to 
        // specify wildcards for the namespace portion of the match
        if (patternNamespace.equals(namespace)) {
            if (log.isDebugEnabled()) {
                log.debug("Successful Match: The local names and namespaces match.");
            }
            return true;
        }
        
        if (log.isDebugEnabled()) {
            log.debug("No Match");
        }
        return false;   
    }
    
    
    private static void validatePattern(QName pattern) {
        if (pattern == null) {
            if (log.isDebugEnabled()) {
                log.debug("The pattern qname is null.  This is accepted and interpretted as a wildcard");
            }
            return;
        }
        String patternStringPrefix = pattern.getPrefix();
        String patternInfoLocalPart = pattern.getLocalPart();
        String patternString = ((patternStringPrefix == null) || (patternStringPrefix.equals(""))) ? "" : patternStringPrefix + ":";
        patternString += patternInfoLocalPart;

        /*
         * Below pattern is ported from:  http://java.sun.com/xml/ns/javaee/javaee_web_services_1_2.xsd
         * Schema regular expressions are defined differently from Java regular expressions which are different from Perl regular
         * expressions.  I've converted the pattern defined in the above linked schema to its Java equivalent, as best as I can.
         * 
         * Schema reg ex:  "\*|([\i-[:]][\c-[:]]*:)?[\i-[:]][\c-[:]]*\*?"
         * Java reg ex:  "\\*|((\\w|_)(\\w|\\.|-|_)*:)?(\\w|_)(\\w|\\.|-|_)*\\*?"
         */

        // first, confirm the defined pattern is legal
        Pattern p = Pattern.compile("\\*|((\\w|_)(\\w|\\.|-|_)*:)?(\\w|_)(\\w|\\.|-|_)*\\*?");
        Matcher m = p.matcher(patternString);
        if (!m.matches()) {
            // pattern defined by user in handler chain xml file is illegal -- report it but continue
            log.warn("Pattern defined by user is illegal:  \"" + patternString + "\" does not match regular expression in schema http://java.sun.com/xml/ns/javaee/javaee_web_services_1_2.xsd.  Pattern matching should now be considered \"best-effort.\"");
        }
    }

    /**
     * Return the class for this name
     *
     * @return Class
     */
    private static Class forName(final String className, final boolean initialize,
                                 final ClassLoader classLoader) throws ClassNotFoundException {
        // NOTE: This method must remain protected because it uses AccessController
        Class cl = null;
        try {
            cl = (Class)AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            try{
                                if (log.isDebugEnabled()) {
                                    log.debug("HandlerResolverImpl attempting to load Class: "+className);
                                }
                                return Class.forName(className, initialize, classLoader);
                            } catch (Throwable e) {
                                // TODO Should the exception be swallowed ?
                                if (log.isDebugEnabled()) {
                                    log.debug("HandlerResolverImpl cannot load the following class Throwable Exception Occured: " + className);
                                }
                                throw new ClassNotFoundException("HandlerResolverImpl cannot load the following class Throwable Exception Occured:" + className);
                            }
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e);
            }
            throw (ClassNotFoundException)e.getException();
        }

        return cl;
    }


    /** @return ClassLoader */
    private static ClassLoader getContextClassLoader() {
        // NOTE: This method must remain private because it uses AccessController
        ClassLoader cl = null;
        try {
            cl = (ClassLoader)AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return Thread.currentThread().getContextClassLoader();
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e);
            }
            throw ExceptionFactory.makeWebServiceException(e.getException());
        }

        return cl;
    }
    
    static {
        protocolBindingsMap.put("##SOAP11_HTTP",        "http://schemas.xmlsoap.org/wsdl/soap/http");
        protocolBindingsMap.put("##SOAP11_HTTP_MTOM",   "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true");
        protocolBindingsMap.put("##SOAP12_HTTP",        "http://www.w3.org/2003/05/soap/bindings/HTTP/");
        protocolBindingsMap.put("##SOAP12_HTTP_MTOM",   "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true");
        protocolBindingsMap.put("##XML_HTTP",           "http://www.w3.org/2004/08/wsdl/http");
    }
}
