/*
 * 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 flex.messaging.config;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.w3c.dom.Node;
import org.w3c.dom.Text;

import flex.messaging.log.Log;
import flex.messaging.log.LogCategories;
import flex.messaging.util.StringUtils;

/**
 * This class is used by configuration parser to replace tokens of the format
 * {...} with actual values. The value can either come from a JVM property
 * (eg. -Dmy.channel.id=my-amf) or from a token properties file specified via
 * token.file JVM property (eg. -Dtoken.file=/usr/local/tokens.properties) where
 * the token key and value are specified in the property file.
 * <p>
 * If a token value is both specified as a JVM property and inside the token.file,
 * JVM property takes precedence.
 */
public class TokenReplacer {
    private static final String TOKEN_FILE = "token.file";
    private static final Pattern pattern = Pattern.compile("\\{(.*?)\\}");

    private final Map replacedTokens;
    private Properties tokenProperties;
    private String tokenPropertiesFilename;

    /**
     * Default constructor.
     */
    public TokenReplacer() {
        replacedTokens = new LinkedHashMap();
        loadTokenProperties();
    }

    /**
     * Replace any tokens in the value of the node or the text child of the node.
     *
     * @param node           The node whose value will be searched for tokens.
     * @param sourceFileName The source file where the node came from.
     */
    public void replaceToken(Node node, String sourceFileName) {
        // Exit if we are attempting to replace one of the forbidden nodes - nodes
        // that may have curly brackets as part of their syntax
        if (ConfigurationConstants.IP_ADDRESS_PATTERN.equals(node.getNodeName()))
            return;

        // ReplacementNode is either the original node if it has a value or the text
        // child of the node if it does not have a value
        Node replacementNode;
        if (node.getNodeValue() == null) {
            if (node.getChildNodes().getLength() == 1 && node.getFirstChild() instanceof Text)
                replacementNode = node.getFirstChild();
            else
                return;
        } else {
            replacementNode = node;
        }

        String nodeValue = replacementNode.getNodeValue();
        Matcher matcher = pattern.matcher(nodeValue);
        while (matcher.find()) // Means the node value has token(s)
        {
            String tokenWithCurlyBraces = matcher.group();
            String tokenWithoutCurlyBraces = matcher.group(1);
            String propertyValue = getPropertyValue(tokenWithoutCurlyBraces);
            if (propertyValue != null) {
                nodeValue = StringUtils.substitute(nodeValue, tokenWithCurlyBraces, propertyValue);
                replacedTokens.put(tokenWithCurlyBraces, propertyValue);
            }
            // context-path, server-name and server-port tokens can be replaced
            // later, therefore, no warning is necessary if they cannot be replaced
            // at this point.
            else if (!ConfigurationConstants.CONTEXT_PATH_TOKEN.equals(tokenWithCurlyBraces)
                    && !ConfigurationConstants.CONTEXT_PATH_ALT_TOKEN.equals(tokenWithCurlyBraces)
                    && !ConfigurationConstants.SERVER_NAME_TOKEN.equals(tokenWithCurlyBraces)
                    && !ConfigurationConstants.SERVER_PORT_TOKEN.equals(tokenWithCurlyBraces)) {
                // Token ''{0}'' in ''{1}'' was not replaced. Either supply a value to this token with a JVM option, or remove it from the configuration.
                ConfigurationException ex = new ConfigurationException();
                Object[] args = {tokenWithCurlyBraces, sourceFileName};
                ex.setMessage(ConfigurationConstants.IRREPLACABLE_TOKEN, args);
                throw ex;
            }
        }
        replacementNode.setNodeValue(nodeValue);
    }

    /**
     * See if the token have a value either provided as a JVM property or as
     * part of the token property file. JVM property takes precedence on the token
     * property file.
     */
    private String getPropertyValue(String tokenWithoutCurlyBraces) {
        String propertyValue = System.getProperty(tokenWithoutCurlyBraces);
        if (propertyValue != null)
            return propertyValue;

        if (tokenProperties != null)
            propertyValue = tokenProperties.getProperty(tokenWithoutCurlyBraces);

        return propertyValue;
    }

    /**
     * Used by the parser to report the replaced tokens once logging is setup.
     */
    public void reportTokens() {
        if (Log.isWarn()) {
            if (tokenProperties != null && Log.isDebug())
                Log.getLogger(LogCategories.CONFIGURATION).debug("Token replacer is using the token file '" + tokenPropertiesFilename + "'");

            for (Iterator iter = replacedTokens.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iter.next();
                String tokenWithParanthesis = (String) entry.getKey();
                String propertyValue = (String) entry.getValue();
                // Issue a special warning for context.root replacements,
                if (ConfigurationConstants.CONTEXT_PATH_TOKEN.equals(tokenWithParanthesis)
                        || ConfigurationConstants.CONTEXT_PATH_ALT_TOKEN.equals(tokenWithParanthesis)) {
                    Log.getLogger(LogCategories.CONFIGURATION).warn("Token '{0}' was replaced with '{1}'. Note that this will apply to all applications on the JVM",
                            new Object[]{tokenWithParanthesis, propertyValue});
                } else if (Log.isDebug()) {
                    Log.getLogger(LogCategories.CONFIGURATION).debug("Token '{0}' was replaced with '{1}'", new Object[]{tokenWithParanthesis, propertyValue});
                }
            }
        }
    }

    /**
     * Given a String, determines whether the string contains tokens.
     *
     * @param value The String to check.
     * @return True if the String contains tokens.
     */
    public static boolean containsTokens(String value) {
        return (value != null && value.length() > 0) ? pattern.matcher(value).find() : false;
    }

    private void loadTokenProperties() {
        tokenPropertiesFilename = System.getProperty(TOKEN_FILE);
        if (tokenPropertiesFilename == null)
            return;

        tokenProperties = new Properties();
        try {
            tokenProperties.load(new FileInputStream(tokenPropertiesFilename));
        } catch (FileNotFoundException e) {
            tokenProperties = null;
        } catch (IOException e) {
            tokenProperties = null;
        }
    }
}
