/*
 * $Id: ActionConfigMatcher.java 530002 2007-04-18 12:35:44Z pbenedict $
 *
 * 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.struts.config;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.util.WildcardHelper;


/**
 * <p> Matches paths against pre-compiled wildcard expressions pulled from
 * action configs. It uses the wildcard matcher from the Apache Cocoon
 * project. Patterns will be matched in the order they exist in the Struts
 * config file. The last match wins, so more specific patterns should be
 * defined after less specific patterns.
 *
 * @since Struts 1.2
 */
public class ActionConfigMatcher implements Serializable {

    /** serialVersionUID */
    private static final long serialVersionUID = 6113399731187154520L;

    /**
     * <p> The logging instance </p>
     */
    private static final Log LOG = LogFactory.getLog(ActionConfigMatcher.class);

    /**
     * <p> Handles all wildcard pattern matching. </p>
     */
    private static final WildcardHelper WILDCARD = new WildcardHelper();

    /**
     * <p> The compiled paths and their associated ActionConfig's </p>
     */
    private final List<Mapping> compiledPaths = new ArrayList<>();

    /**
     * <p> Finds and precompiles the wildcard patterns from the ActionConfig
     * "path" attributes. ActionConfig's will be evaluated in the order they
     * exist in the Struts config file. Only paths that actually contain a
     * wildcard will be compiled. </p>
     *
     * @param configs An array of ActionConfig's to process
     */
    public ActionConfigMatcher(final ActionConfig[] configs) {

        for (final ActionConfig config : configs) {
            String path = config.getPath();
            if ((path != null) && (path.indexOf('*') > -1)) {
                if ((path.length() > 0) && (path.charAt(0) == '/')) {
                    path = path.substring(1);
                }

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Compiling action config path '" + path + "'");
                }

                int[] pattern = WILDCARD.compilePattern(path);
                this.compiledPaths.add(new Mapping(pattern, config));
            }
        }
    }

    /**
     * <p> Matches the path against the compiled wildcard patterns. </p>
     *
     * @param path The portion of the request URI for selecting a config.
     * @return The action config if matched, else null
     */
    public ActionConfig match(final String path) {
        ActionConfig config = null;

        if (this.compiledPaths.size() > 0) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Attempting to match '" + path + "' to a wildcard pattern");
            }

            String pt = path;
            if ((pt.length() > 0) && (pt.charAt(0) == '/')) {
                pt = path.substring(1);
            }

            Map<String, String> vars = new HashMap<>();
            for (final Mapping m : this.compiledPaths) {
                if (WILDCARD.match(vars, pt, m.getPattern())) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Path matches pattern '"
                            + m.getActionConfig().getPath() + "'");
                    }

                    config = convertActionConfig(pt, m.getActionConfig(), vars);
                }
            }
        }

        return config;
    }

    /**
     * <p> Clones the ActionConfig and its children, replacing various
     * properties with the values of the wildcard-matched strings. </p>
     *
     * @param path The requested path
     * @param orig The original ActionConfig
     * @param vars A Map of wildcard-matched strings
     * @return A cloned ActionConfig with appropriate properties replaced with
     *         wildcard-matched values
     */
    protected ActionConfig convertActionConfig(final String path, final ActionConfig orig,
            final Map<String, String> vars) {

        final ActionConfig config;

        try {
            config = (ActionConfig) BeanUtils.cloneBean(orig);
        } catch (final ReflectiveOperationException ex) {
            LOG.warn("Unable to clone action config, recommend not using "
                + "wildcards", ex);
            return null;
        }

        setActionConfigTo(config, path, orig, vars);

        ForwardConfig[] fConfigs = orig.findForwardConfigs();

        for (final ForwardConfig fConfig : fConfigs) {
            ForwardConfig cfg;

            try {
                cfg = (ForwardConfig) BeanUtils.cloneBean(fConfig);
            } catch (final ReflectiveOperationException ex) {
                LOG.warn("Unable to clone action config, recommend not using "
                        + "wildcards", ex);
                return null;
            }

            setForwardConfigTo(cfg, fConfig, vars);

            replaceProperties(fConfig.getProperties(), cfg.getProperties(), vars);

            config.removeForwardConfig(fConfig);
            config.addForwardConfig(cfg);
        }

        replaceProperties(orig.getProperties(), config.getProperties(), vars);

        for (final ExceptionConfig exConfig : orig.findExceptionConfigs()) {
            config.addExceptionConfig(exConfig);
        }

        config.freeze();

        return config;
    }

    /**
     * set to ActionConfig
     *
     * @param config ActionConfig
     * @param path Path
     * @param orig ActionConfig
     * @param vars Map
     */
    private void setActionConfigTo(final ActionConfig config, final String path,
            final ActionConfig orig, final Map<String, String> vars) {

        String pt = path;
        if ((pt.isEmpty()) || (pt.charAt(0) != '/')) {
            pt = "/" + pt;
        }

        config.setPath(pt);
        config.setName(convertParam(orig.getName(), vars));
        config.setType(convertParam(orig.getType(), vars));
        config.setRoles(convertParam(orig.getRoles(), vars));
        config.setParameter(convertParam(orig.getParameter(), vars));
        config.setAttribute(convertParam(orig.getAttribute(), vars));
        config.setForward(convertParam(orig.getForward(), vars));
        config.setInclude(convertParam(orig.getInclude(), vars));
        config.setInput(convertParam(orig.getInput(), vars));
        config.setCatalog(convertParam(orig.getCatalog(), vars));
        config.setCommand(convertParam(orig.getCommand(), vars));
        config.setMultipartClass(convertParam(orig.getMultipartClass(), vars));
        config.setPrefix(convertParam(orig.getPrefix(), vars));
        config.setSuffix(convertParam(orig.getSuffix(), vars));
    }

    /**
     * set to ForwardConfig
     *
     * @param cfg ForwardConfig
     * @param fConfig fConfig
     * @param vars Map
     */
    private void setForwardConfigTo(final ForwardConfig cfg,
            final ForwardConfig fConfig, final Map<String, String> vars) {
        cfg.setName(fConfig.getName());
        cfg.setPath(convertParam(fConfig.getPath(), vars));
        cfg.setRedirect(fConfig.getRedirect());
        cfg.setCommand(convertParam(fConfig.getCommand(), vars));
        cfg.setCatalog(convertParam(fConfig.getCatalog(), vars));
        cfg.setModule(convertParam(fConfig.getModule(), vars));
    }

    /**
     * <p> Replaces placeholders from one Properties values set to another.
     * </p>
     *
     * @param orig  The original properties set with placeholder values
     * @param props The target properties to store the processed values
     * @param vars  A Map of wildcard-matched strings
     */
    protected void replaceProperties(final Properties orig, final Properties props,
            final Map<String, String> vars) {

        for (final String key : orig.stringPropertyNames()) {
            props.setProperty(key, convertParam(orig.getProperty(key), vars));
        }
    }

    /**
     * <p> Inserts into a value wildcard-matched strings where specified.
     * </p>
     *
     * @param val  The value to convert
     * @param vars A Map of wildcard-matched strings
     * @return The new value
     */
    protected String convertParam(final String val, final Map<String, String> vars) {
        if (val == null) {
            return null;
        } else if (val.indexOf('{') == -1) {
            return val;
        }

        StringBuilder key = new StringBuilder("{0}");
        StringBuilder ret = new StringBuilder(val);
        int x;

        for (final Entry<String, String> entry : vars.entrySet()) {

            key.setCharAt(1, entry.getKey().charAt(0));
            String keyTmp = key.toString();

            // Replace all instances of the placeholder
            while ((x = ret.toString().indexOf(keyTmp)) > -1) {
                ret.replace(x, x + 3, entry.getValue());
            }
        }

        return ret.toString();
    }

    /**
     * <p> Stores a compiled wildcard pattern and the ActionConfig it came
     * from. </p>
     */
    private static class Mapping implements Serializable {

        /** serialVersionUID */
        private static final long serialVersionUID = 7734770683610820209L;

        /**
         * <p> The compiled pattern. </p>
         */
        private final int[] pattern;

        /**
         * <p> The original ActionConfig. </p>
         */
        private final ActionConfig config;

        /**
         * <p> Constructs a read-only Mapping instance. </p>
         *
         * @param val1 The compiled pattern
         * @param val2  The original ActionConfig
         */
        Mapping(final int[] val1, final ActionConfig val2) {
            this.pattern = val1;
            this.config = val2;
        }

        /**
         * <p> Gets the compiled wildcard pattern. </p>
         *
         * @return The compiled pattern
         */
        public int[] getPattern() {
            return this.pattern;
        }

        /**
         * <p> Gets the ActionConfig that contains the pattern. </p>
         *
         * @return The associated ActionConfig
         */
        public ActionConfig getActionConfig() {
            return this.config;
        }
    }
}
