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

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.LogicalHandler;

import org.apache.cxf.Bus;
import org.apache.cxf.common.injection.ResourceInjector;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.jaxws.handler.types.ParamValueType;
import org.apache.cxf.jaxws.handler.types.PortComponentHandlerType;
import org.apache.cxf.resource.ResourceManager;
import org.apache.cxf.resource.ResourceResolver;

@SuppressWarnings("rawtypes")
public class HandlerChainBuilder {
    static final Logger LOG = LogUtils.getL7dLogger(HandlerChainBuilder.class);
    private static final ResourceBundle BUNDLE = LOG.getResourceBundle();

    private Bus bus;
    private boolean handlerInitEnabled = true;

    public HandlerChainBuilder(Bus aBus) {
        bus = aBus;
    }

    public HandlerChainBuilder() {
        this(null);
    }

    public List<Handler> buildHandlerChainFromConfiguration(PortComponentHandlerType hc) {
        if (null == hc) {
            return null;
        }
        return sortHandlers(buildHandlerChain(hc, getHandlerClassLoader()));
    }
    public List<Handler> buildHandlerChainFromConfiguration(List<PortComponentHandlerType> hc) {
        if (null == hc || hc.isEmpty()) {
            return null;
        }
        List<Handler> handlers = new ArrayList<>();
        for (PortComponentHandlerType pt : hc) {
            handlers.addAll(buildHandlerChain(pt, getHandlerClassLoader()));
        }
        return sortHandlers(handlers);
    }

    // methods used by Geronimo to allow configuring things themselves
    public void setHandlerInitEnabled(boolean b) {
        handlerInitEnabled = b;
    }

    public boolean isHandlerInitEnabled() {
        return handlerInitEnabled;
    }

    /**
     * sorts the handlers into correct order. All of the logical handlers first
     * followed by the protocol handlers
     *
     * @param handlers
     * @return sorted list of handlers
     */
    public List<Handler> sortHandlers(List<Handler> handlers) {

        final int size = handlers.size();
        List<Handler> logicalHandlers = new ArrayList<>(size);
        List<Handler> protocolHandlers = new ArrayList<>(Math.min(10, size));

        for (Handler<?> handler : handlers) {
            if (handler instanceof LogicalHandler) {
                logicalHandlers.add(handler);
            } else {
                protocolHandlers.add(handler);
            }
        }

        if (!protocolHandlers.isEmpty()) {
            logicalHandlers.addAll(protocolHandlers);
        }
        return logicalHandlers;
    }

    protected ClassLoader getHandlerClassLoader() {
        return getClass().getClassLoader();
    }

    protected List<Handler> buildHandlerChain(PortComponentHandlerType ht, ClassLoader classLoader) {
        List<Handler> handlerChain = new ArrayList<>();
        try {
            final boolean fineLog = LOG.isLoggable(Level.FINE);
            if (fineLog) {
                LOG.log(Level.FINE, "loading handler", trimString(ht.getHandlerName().getValue()));
            }

            Class<? extends Handler> handlerClass = Class.forName(
                                                                  trimString(ht.getHandlerClass()
                                                                      .getValue()), true, classLoader)
                .asSubclass(Handler.class);

            Handler<?> handler = handlerClass.getDeclaredConstructor().newInstance();
            if (fineLog) {
                LOG.fine("adding handler to chain: " + handler);
            }
            configureHandler(handler, ht);
            handlerChain.add(handler);
        } catch (Exception e) {
            throw new WebServiceException(BUNDLE.getString("HANDLER_INSTANTIATION_EXC"), e);
        }
        return handlerChain;
    }

    /**
     * Resolve handler chain configuration file associated with the given class
     *
     * @param clz
     * @param filename
     * @return A URL object or null if no resource with this name is found
     */
    protected URL resolveHandlerChainFile(Class<?> clz, String filename) {
        URL handlerFile = clz.getResource(filename);
        if (handlerFile == null) {
            //the file location might be an absolute java.net.URL in externalForm.
            try {
                handlerFile = new URL(filename);
                //test if the URL can be opened
                handlerFile.openStream();
            } catch (Exception e) {
                //do nothing
            }
        }
        return handlerFile;
    }

    private void configureHandler(Handler<?> handler, PortComponentHandlerType h) {
        if (!handlerInitEnabled) {
            return;
        }

        if (h.getInitParam().isEmpty()) {
            return;
        }

        Map<String, String> params = new HashMap<>();

        for (ParamValueType param : h.getInitParam()) {
            params.put(trimString(param.getParamName() == null ? null : param.getParamName().getValue()),
                       trimString(param.getParamValue() == null ? null : param.getParamValue().getValue()));
        }

        Method initMethod = getInitMethod(handler);
        if (initMethod != null) {
            initializeViaInitMethod(handler, params, initMethod);
        } else {
            initializeViaInjection(handler, params);
        }
    }

    private void initializeViaInjection(Handler<?> handler, final Map<String, String> params) {
        if (bus != null) {
            ResourceManager resMgr = bus.getExtension(ResourceManager.class);
            List<ResourceResolver> resolvers = resMgr.getResourceResolvers();
            resolvers.add(new InitParamResourceResolver(params));
            ResourceInjector resInj = new ResourceInjector(resMgr, resolvers);
            resInj.inject(handler);
            resInj.construct(handler);
        }
    }

    private void initializeViaInitMethod(Handler<?> handler, Map<String, String> params, Method init) {
        try {
            init.invoke(handler, params);
        } catch (InvocationTargetException ex) {
            Throwable t = ex.getCause() != null ? ex.getCause() : ex;
            LogUtils.log(LOG, Level.WARNING, "INIT_METHOD_THREW_EXCEPTION", t, handler.getClass());
        } catch (IllegalAccessException ex) {
            LOG.log(Level.SEVERE, "CANNOT_ACCESS_INIT", handler.getClass());
        }
    }

    private Method getInitMethod(Handler<?> handler) {
        Method m = null;
        try {
            m = handler.getClass().getMethod("init", Map.class);
        } catch (NoSuchMethodException ex) {
            // empty
        }
        return m;
    }

    private String trimString(String str) {
        return str != null ? str.trim() : null;
    }
}
