<commit_before>/*
 * File   : $Source: /alkacon/cvs/opencms/src/org/opencms/util/CmsMacroResolver.java,v $
 * Date   : $Date: 2010/01/26 11:00:38 $
 * Version: $Revision: 1.7 $
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) 2002 - 2009 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.util;

import org.opencms.file.CmsObject;
import org.opencms.file.CmsProperty;
import org.opencms.file.CmsResource;
import org.opencms.flex.CmsFlexController;
import org.opencms.i18n.CmsMessageContainer;
import org.opencms.i18n.CmsMessages;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.security.CmsOrganizationalUnit;
import org.opencms.xml.containerpage.CmsContainerElementBean;
import org.opencms.xml.sitemap.CmsSiteEntryBean;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.jsp.PageContext;

import org.apache.commons.logging.Log;

/**
 * Resolves macros in the form of <code>%(key)</code> or <code>${key}</code> in an input String.<p>
 * 
 * Starting with OpenCms 7.0, the preferred form of a macro is <code>%(key)</code>. This is to 
 * avoid conflicts / confusion with the JSP EL, which also uses the <code>${key}</code> syntax.<p>
 * 
 * The macro names that can be resolved depend of the context objects provided to the resolver
 * using the <code>set...</code> methods.<p>
 * 
 * @author <NAME> 
 * @author <NAME>  
 * 
 * @version $Revision: 1.7 $ 
 * 
 * @since 6.0.0 
 */
public class CmsMacroResolver implements I_CmsMacroResolver {

    /** The prefix indicating that the key represents an OpenCms runtime attribute. */
    public static final String KEY_ATTRIBUTE = "attribute.";

    /** Key used to specify the context path as macro value. */
    public static final String KEY_CONTEXT_PATH = "contextPath";

    /** Key used to specify the description of the current organizational unit as macro value. */
    public static final String KEY_CURRENT_ORGUNIT_DESCRIPTION = "currentou.description";

    /** Key used to specify the full qualified name of the current organizational unit as macro value. */
    public static final String KEY_CURRENT_ORGUNIT_FQN = "currentou.fqn";

    /** Key used to specify the current time as macro value. */
    public static final String KEY_CURRENT_TIME = "currenttime";

    /** Key used to specify the city of the current user as macro value. */
    public static final String KEY_CURRENT_USER_CITY = "currentuser.city";

    /** Key used to specify the country of the current user as macro value. */
    public static final String KEY_CURRENT_USER_COUNTRY = "currentuser.country";

    /** Key used to specify the display name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_DISPLAYNAME = "currentuser.displayname";

    /** Key used to specify the email address of the current user as macro value. */
    public static final String KEY_CURRENT_USER_EMAIL = "currentuser.email";

    /** Key used to specify the first name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_FIRSTNAME = "currentuser.firstname";

    /** Key used to specify the full name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_FULLNAME = "currentuser.fullname";

    /** Key used to specify the last login date of the current user as macro value. */
    public static final String KEY_CURRENT_USER_LASTLOGIN = "currentuser.lastlogin";

    /** Key used to specify the last name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_LASTNAME = "currentuser.lastname";

    /** Key used to specify the user name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_NAME = "currentuser.name";

    /** Key used to specify the street of the current user as macro value. */
    public static final String KEY_CURRENT_USER_STREET = "currentuser.street";

    /** Key used to specify the zip code of the current user as macro value. */
    public static final String KEY_CURRENT_USER_ZIP = "currentuser.zip";

    /** Key prefix used to specify the value of a localized key as macro value. */
    public static final String KEY_LOCALIZED_PREFIX = "key.";

    /** Identifier for "magic" parameter names. */
    public static final String KEY_OPENCMS = "opencms.";

    /** The prefix indicating that the key represents a page context object. */
    public static final String KEY_PAGE_CONTEXT = "pageContext.";

    /** Key used to specify the project id as macro value. */
    public static final String KEY_PROJECT_ID = "projectid";

    /** The prefix indicating that the key represents a property to be read on the current request URI. */
    public static final String KEY_PROPERTY = "property.";

    /** The prefix indicating that the key represents a property to be read on the current element. */
    public static final String KEY_PROPERTY_ELEMENT = "elementProperty.";

    /** Key used to specify the request encoding as macro value. */
    public static final String KEY_REQUEST_ENCODING = "request.encoding";

    /** Key used to specify the folder of the request URI as macro value. */
    public static final String KEY_REQUEST_FOLDER = "request.folder";

    /** Key user to specify the request locale as macro value. */
    public static final String KEY_REQUEST_LOCALE = "request.locale";

    /** The prefix indicating that the key represents a HTTP request parameter. */
    public static final String KEY_REQUEST_PARAM = "param.";

    /** Key used to specify the request uri as macro value. */
    public static final String KEY_REQUEST_URI = "request.uri";

    /** Key used to specify the validation path as macro value. */
    public static final String KEY_VALIDATION_PATH = "validation.path";

    /** Key used to specify the validation regex as macro value. */
    public static final String KEY_VALIDATION_REGEX = "validation.regex";

    /** Key used to specify the validation value as macro value. */
    public static final String KEY_VALIDATION_VALUE = "validation.value";

    /** Identified for "magic" parameter commands. */
    static final String[] VALUE_NAMES_ARRAY = {"uri", // 0
        "filename", // 1  
        "folder", // 2 
        "default.encoding", // 3  
        "remoteaddress", // 4
        "webapp", // 5
        "webbasepath", // 6 
        "version", // 7
        "element", // 8
        "sitemap" // 9
    };

    /** The "magic" commands wrapped in a List. */
    public static final List<String> VALUE_NAMES = Collections.unmodifiableList(Arrays.asList(VALUE_NAMES_ARRAY));

    /** The log object for this class. */
    private static final Log LOG = CmsLog.getLog(CmsMacroResolver.class);

    /** A map of additional values provided by the calling class. */
    protected Map<String, String> m_additionalMacros;

    /** The OpenCms user context to use for resolving macros. */
    protected CmsObject m_cms;

    /** The JSP's page context to use for resolving macros. */
    protected PageContext m_jspPageContext;

    /** Indicates if unresolved macros should be kept "as is" or replaced by an empty String. */
    protected boolean m_keepEmptyMacros;

    /** The messages resource bundle to resolve localized keys with. */
    protected CmsMessages m_messages;

    /** The resource name to use for resolving macros. */
    protected String m_resourceName;

    /**
     * Adds macro delimiters to the given input, 
     * for example <code>key</code> becomes <code>%(key)</code>.<p>
     * 
     * @param input the input to format as a macro
     * 
     * @return the input formatted as a macro
     */
    public static String formatMacro(String input) {

        StringBuffer result = new StringBuffer(input.length() + 4);
        result.append(I_CmsMacroResolver.MACRO_DELIMITER);
        result.append(I_CmsMacroResolver.MACRO_START);
        result.append(input);
        result.append(I_CmsMacroResolver.MACRO_END);
        return result.toString();
    }

    /**
     * Returns <code>true</code> if the given input String if formatted like a macro,
     * that is it starts with <code>{@link I_CmsMacroResolver#MACRO_DELIMITER_OLD} +
     * {@link I_CmsMacroResolver#MACRO_START_OLD}</code> and ends with 
     * <code>{@link I_CmsMacroResolver#MACRO_END_OLD}</code>.<p>
     * 
     * @param input the input to check for a macro
     * @return <code>true</code> if the given input String if formatted like a macro
     */
    public static boolean isMacro(String input) {

        if (CmsStringUtil.isEmpty(input) || (input.length() < 3)) {
            return false;
        }

        return (((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER_OLD) && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START_OLD) && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END_OLD))) || ((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER) && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START) && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END))));
    }

    /**
     * Returns <code>true</code> if the given input String is a macro equal to the given macro name.<p>
     * 
     * @param input the input to check for a macro
     * @param macroName the macro name to check for
     * 
     * @return <code>true</code> if the given input String is a macro equal to the given macro name
     */
    public static boolean isMacro(String input, String macroName) {

        if (isMacro(input)) {
            return input.substring(2, input.length() - 1).equals(macroName);
        }
        return false;
    }

    /**
     * Returns a macro for the given localization key with the given parameters.<p>
     * 
     * @param keyName the name of the localized key
     * @param params the optional parameter array
     * 
     * @return a macro for the given localization key with the given parameters
     */
    public static String localizedKeyMacro(String keyName, Object[] params) {

        String parameters = "";
        if ((params != null) && (params.length > 0)) {
            for (int i = 0; i < params.length; i++) {
                if (params[i] != null) {
                    parameters += "|" + params[i].toString();
                }
            }
        }
        return ""
            + I_CmsMacroResolver.MACRO_DELIMITER
            + I_CmsMacroResolver.MACRO_START
            + CmsMacroResolver.KEY_LOCALIZED_PREFIX
            + keyName
            + parameters
            + I_CmsMacroResolver.MACRO_END;
    }

    /**
     * Factory method to create a new {@link CmsMacroResolver} instance.<p>
     * 
     * @return a new instance of a {@link CmsMacroResolver}
     */
    public static CmsMacroResolver newInstance() {

        return new CmsMacroResolver();
    }

    /**
     * Resolves the macros in the given input using the provided parameters.<p>
     * 
     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given 
     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
     * 
     * If a macro is found that can not be mapped to a value by the given macro resolver,
     * it is left untouched in the input.<p>
     * 
     * @param input the input in which to resolve the macros
     * @param cms the OpenCms user context to use when resolving macros
     * @param messages the message resource bundle to use when resolving macros
     * 
     * @return the input with the macros resolved
     */
    public static String resolveMacros(String input, CmsObject cms, CmsMessages messages) {

        CmsMacroResolver resolver = new CmsMacroResolver();
        resolver.m_cms = cms;
        resolver.m_messages = messages;
        resolver.m_keepEmptyMacros = true;
        return resolver.resolveMacros(input);
    }

    /**
     * Resolves macros in the provided input String using the given macro resolver.<p>
     * 
     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given 
     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
     * 
     * If a macro is found that can not be mapped to a value by the given macro resolver,
     * <code>{@link I_CmsMacroResolver#isKeepEmptyMacros()}</code> controls if the macro is replaced by
     * an empty String, or is left untouched in the input.<p>
     * 
     * @param input the input in which to resolve the macros
     * @param resolver the macro resolver to use
     * 
     * @return the input with all macros resolved
     */
    public static String resolveMacros(final String input, I_CmsMacroResolver resolver) {

        if ((input == null) || (input.length() < 3)) {
            // macro must have at last 3 chars "${}" or "%()"
            return input;
        }

        int pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER);
        int po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD);

        if ((po == -1) && (pn == -1)) {
            // no macro delimiter found in input
            return input;
        }

        int len = input.length();
        StringBuffer result = new StringBuffer(len << 1);
        int np, pp1, pp2, e;
        String macro, value;
        boolean keep = resolver.isKeepEmptyMacros();
        boolean resolvedNone = true;
        char ds, de;
        int p;

        if ((po == -1) || ((pn > -1) && (pn < po))) {
            p = pn;
            ds = I_CmsMacroResolver.MACRO_START;
            de = I_CmsMacroResolver.MACRO_END;
        } else {
            p = po;
            ds = I_CmsMacroResolver.MACRO_START_OLD;
            de = I_CmsMacroResolver.MACRO_END_OLD;
        }

        // append chars before the first delimiter found
        result.append(input.substring(0, p));
        do {
            pp1 = p + 1;
            pp2 = pp1 + 1;
            if (pp2 >= len) {
                // remaining chars can't be a macro (minimum size is 3)
                result.append(input.substring(p, len));
                break;
            }
            // get the next macro delimiter
            if ((pn > -1) && (pn < pp1)) {
                pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER, pp1);
            }
            if ((po > -1) && (po < pp1)) {
                po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD, pp1);
            }
            if ((po == -1) && (pn == -1)) {
                // none found, make sure remaining chars in this segment are appended
                np = len;
            } else {
                // check if the next delimiter is old or new style
                if ((po == -1) || ((pn > -1) && (pn < po))) {
                    np = pn;
                } else {
                    np = po;
                }
            }
            // check if the next char is a "macro start"
            char st = input.charAt(pp1);
            if (st == ds) {
                // we have a starting macro sequence "${" or "%(", now check if this segment contains a "}" or ")"
                e = input.indexOf(de, p);
                if ((e > 0) && (e < np)) {
                    // this segment contains a closing macro delimiter "}" or "]", so we may have found a macro
                    macro = input.substring(pp2, e);
                    // resolve macro
                    value = resolver.getMacroValue(macro);
                    e++;
                    if (value != null) {
                        // macro was successfully resolved
                        result.append(value);
                        resolvedNone = false;
                    } else if (keep) {
                        // macro was unknown, but should be kept
                        result.append(input.substring(p, e));
                    }
                } else {
                    // no complete macro "${...}" or "%(...)" in this segment
                    e = p;
                }
            } else {
                // no macro start char after the "$" or "%"
                e = p;
            }
            // set macro style for next delimiter found
            if (np == pn) {
                ds = I_CmsMacroResolver.MACRO_START;
                de = I_CmsMacroResolver.MACRO_END;
            } else {
                ds = I_CmsMacroResolver.MACRO_START_OLD;
                de = I_CmsMacroResolver.MACRO_END_OLD;
            }
            // append the remaining chars after the macro to the start of the next macro
            result.append(input.substring(e, np));
            // this is a nerdy joke ;-)
            p = np;
        } while (p < len);

        if (resolvedNone && keep) {
            // nothing was resolved and macros should be kept, return original input
            return input;
        }

        // input was changed during resolving of macros
        return result.toString();
    }

    /**
     * Strips the macro delimiters from the given input, 
     * for example <code>%(key)</code> or <code>${key}</code> becomes <code>key</code>.<p>
     * 
     * In case the input is not a macro, <code>null</code> is returned.<p>
     * 
     * @param input the input to strip
     * 
     * @return the macro stripped from the input, or <code>null</code>
     */
    public static String stripMacro(String input) {

        if (isMacro(input)) {
            return input.substring(2, input.length() - 1);
        }
        return null;
    }

    /**
     * Adds a customized macro to this macro resolver.<p>
     * 
     * @param key the macro to add
     * @param value the value to return if the macro is encountered
     */
    public void addMacro(String key, String value) {

        if (m_additionalMacros == null) {
            // use lazy initializing
            m_additionalMacros = new HashMap<String, String>();
        }
        m_additionalMacros.put(key, value);
    }

    /**
     * @see org.opencms.util.I_CmsMacroResolver#getMacroValue(java.lang.String)
     */
    public String getMacroValue(String macro) {

        if (m_messages != null) {
            if (macro.startsWith(CmsMacroResolver.KEY_LOCALIZED_PREFIX)) {
                String keyName = macro.substring(CmsMacroResolver.KEY_LOCALIZED_PREFIX.length());
                return m_messages.keyWithParams(keyName);
            }
        }

        if (m_jspPageContext != null) {

            if (m_jspPageContext.getRequest() != null) {
                if (macro.startsWith(CmsMacroResolver.KEY_REQUEST_PARAM)) {
                    // the key is a request parameter  
                    macro = macro.substring(CmsMacroResolver.KEY_REQUEST_PARAM.length());
                    String result = m_jspPageContext.getRequest().getParameter(macro);
                    if ((result == null) && macro.equals(KEY_PROJECT_ID)) {
                        result = m_cms.getRequestContext().currentProject().getUuid().toString();
                    }
                    return result;
                }

                if ((m_cms != null) && macro.startsWith(CmsMacroResolver.KEY_PROPERTY_ELEMENT)) {

                    // the key is a cms property to be read on the current element

                    macro = macro.substring(CmsMacroResolver.KEY_PROPERTY_ELEMENT.length());
                    CmsFlexController controller = CmsFlexController.getController(m_jspPageContext.getRequest());
                    try {
                        CmsProperty property = m_cms.readPropertyObject(
                            controller.getCurrentRequest().getElementUri(),
                            macro,
                            false);
                        if (property != CmsProperty.getNullProperty()) {
                            return property.getValue();
                        }
                    } catch (CmsException e) {
                        if (LOG.isWarnEnabled()) {
                            LOG.warn(Messages.get().getBundle().key(
                                Messages.LOG_PROPERTY_READING_FAILED_2,
                                macro,
                                controller.getCurrentRequest().getElementUri()), e);
                        }
                    }
                }
            }

            if (macro.startsWith(CmsMacroResolver.KEY_PAGE_CONTEXT)) {
                // the key is a page context object
                macro = macro.substring(CmsMacroResolver.KEY_PAGE_CONTEXT.length());
                int scope = m_jspPageContext.getAttributesScope(macro);
                return m_jspPageContext.getAttribute(macro, scope).toString();
            }
        }

        if (m_cms != null) {

            if (macro.startsWith(CmsMacroResolver.KEY_PROPERTY)) {
                // the key is a cms property to be read on the current request URI
                macro = macro.substring(CmsMacroResolver.KEY_PROPERTY.length());
                try {
                    CmsProperty property = m_cms.readPropertyObject(m_cms.getRequestContext().getUri(), macro, true);
                    if (property != CmsProperty.getNullProperty()) {
                        return property.getValue();
                    }
                } catch (CmsException e) {
                    if (LOG.isWarnEnabled()) {
                        CmsMessageContainer message = Messages.get().container(
                            Messages.LOG_PROPERTY_READING_FAILED_2,
                            macro,
                            m_cms.getRequestContext().getUri());
                        LOG.warn(message.key(), e);
                    }
                }
                return null;
            }

            if (macro.startsWith(CmsMacroResolver.KEY_ATTRIBUTE)) {
                // the key is an OpenCms runtime attribute
                macro = macro.substring(CmsMacroResolver.KEY_ATTRIBUTE.length());
                Object attribute = m_cms.getRequestContext().getAttribute(macro);
                if (attribute != null) {
                    return attribute.toString();
                }
                return null;
            }

            if (macro.startsWith(CmsMacroResolver.KEY_OPENCMS)) {

                // the key is a shortcut for a cms runtime value

                String originalKey = macro;
                macro = macro.substring(CmsMacroResolver.KEY_OPENCMS.length());
                int index = VALUE_NAMES.indexOf(macro);
                String value = null;

                switch (index) {
                    case 0:
                        // "uri"
                        value = m_cms.getRequestContext().getUri();
                        break;
                    case 1:
                        // "filename"
                        value = m_resourceName;
                        break;
                    case 2:
                        // folder
                        value = m_cms.getRequestContext().getFolderUri();
                        break;
                    case 3:
                        // default.encoding
                        value = OpenCms.getSystemInfo().getDefaultEncoding();
                        break;
                    case 4:
                        // remoteaddress
                        value = m_cms.getRequestContext().getRemoteAddress();
                        break;
                    case 5:
                        // webapp
                        value = OpenCms.getSystemInfo().getWebApplicationName();
                        break;
                    case 6:
                        // webbasepath
                        value = OpenCms.getSystemInfo().getWebApplicationRfsPath();
                        break;
                    case 7:
                        // version
                        value = OpenCms.getSystemInfo().getVersionNumber();
                        break;
                    case 8:
                        // element
                        if (m_jspPageContext != null) {
                            try {
                                CmsContainerElementBean element = OpenCms.getADEManager().getCurrentElement(
                                    m_jspPageContext.getRequest());
                                value = m_cms.getSitePath(m_cms.readResource(element.getElementId()));
                            } catch (CmsException e) {
                                if (!LOG.isDebugEnabled()) {
                                    LOG.warn(e.getLocalizedMessage());
                                }
                                LOG.debug(e.getLocalizedMessage(), e);
                            }
                        }
                        break;
                    case 9:
                        // sitemap
                        CmsSiteEntryBean sitemap = OpenCms.getSitemapManager().getRuntimeInfo(
                            m_jspPageContext.getRequest());
                        value = m_cms.getRequestContext().getUri();
                        if (sitemap != null) {
                            value = sitemap.getUri();
                        }
                        break;
                    default:
                        // return the key "as is"
                        value = originalKey;
                        break;
                }

                return value;
            }<commit_msg>added element site path to the element bean only while rendering<commit_after>/*
 * File   : $Source: /alkacon/cvs/opencms/src/org/opencms/util/CmsMacroResolver.java,v $
 * Date   : $Date: 2010/01/26 14:55:22 $
 * Version: $Revision: 1.8 $
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) 2002 - 2009 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.util;

import org.opencms.file.CmsObject;
import org.opencms.file.CmsProperty;
import org.opencms.file.CmsResource;
import org.opencms.flex.CmsFlexController;
import org.opencms.i18n.CmsMessageContainer;
import org.opencms.i18n.CmsMessages;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.security.CmsOrganizationalUnit;
import org.opencms.xml.containerpage.CmsContainerElementBean;
import org.opencms.xml.sitemap.CmsSiteEntryBean;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.jsp.PageContext;

import org.apache.commons.logging.Log;

/**
 * Resolves macros in the form of <code>%(key)</code> or <code>${key}</code> in an input String.<p>
 * 
 * Starting with OpenCms 7.0, the preferred form of a macro is <code>%(key)</code>. This is to 
 * avoid conflicts / confusion with the JSP EL, which also uses the <code>${key}</code> syntax.<p>
 * 
 * The macro names that can be resolved depend of the context objects provided to the resolver
 * using the <code>set...</code> methods.<p>
 * 
 * @author <NAME> 
 * @author <NAME>  
 * 
 * @version $Revision: 1.8 $ 
 * 
 * @since 6.0.0 
 */
public class CmsMacroResolver implements I_CmsMacroResolver {

    /** The prefix indicating that the key represents an OpenCms runtime attribute. */
    public static final String KEY_ATTRIBUTE = "attribute.";

    /** Key used to specify the context path as macro value. */
    public static final String KEY_CONTEXT_PATH = "contextPath";

    /** Key used to specify the description of the current organizational unit as macro value. */
    public static final String KEY_CURRENT_ORGUNIT_DESCRIPTION = "currentou.description";

    /** Key used to specify the full qualified name of the current organizational unit as macro value. */
    public static final String KEY_CURRENT_ORGUNIT_FQN = "currentou.fqn";

    /** Key used to specify the current time as macro value. */
    public static final String KEY_CURRENT_TIME = "currenttime";

    /** Key used to specify the city of the current user as macro value. */
    public static final String KEY_CURRENT_USER_CITY = "currentuser.city";

    /** Key used to specify the country of the current user as macro value. */
    public static final String KEY_CURRENT_USER_COUNTRY = "currentuser.country";

    /** Key used to specify the display name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_DISPLAYNAME = "currentuser.displayname";

    /** Key used to specify the email address of the current user as macro value. */
    public static final String KEY_CURRENT_USER_EMAIL = "currentuser.email";

    /** Key used to specify the first name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_FIRSTNAME = "currentuser.firstname";

    /** Key used to specify the full name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_FULLNAME = "currentuser.fullname";

    /** Key used to specify the last login date of the current user as macro value. */
    public static final String KEY_CURRENT_USER_LASTLOGIN = "currentuser.lastlogin";

    /** Key used to specify the last name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_LASTNAME = "currentuser.lastname";

    /** Key used to specify the user name of the current user as macro value. */
    public static final String KEY_CURRENT_USER_NAME = "currentuser.name";

    /** Key used to specify the street of the current user as macro value. */
    public static final String KEY_CURRENT_USER_STREET = "currentuser.street";

    /** Key used to specify the zip code of the current user as macro value. */
    public static final String KEY_CURRENT_USER_ZIP = "currentuser.zip";

    /** Key prefix used to specify the value of a localized key as macro value. */
    public static final String KEY_LOCALIZED_PREFIX = "key.";

    /** Identifier for "magic" parameter names. */
    public static final String KEY_OPENCMS = "opencms.";

    /** The prefix indicating that the key represents a page context object. */
    public static final String KEY_PAGE_CONTEXT = "pageContext.";

    /** Key used to specify the project id as macro value. */
    public static final String KEY_PROJECT_ID = "projectid";

    /** The prefix indicating that the key represents a property to be read on the current request URI. */
    public static final String KEY_PROPERTY = "property.";

    /** The prefix indicating that the key represents a property to be read on the current element. */
    public static final String KEY_PROPERTY_ELEMENT = "elementProperty.";

    /** Key used to specify the request encoding as macro value. */
    public static final String KEY_REQUEST_ENCODING = "request.encoding";

    /** Key used to specify the folder of the request URI as macro value. */
    public static final String KEY_REQUEST_FOLDER = "request.folder";

    /** Key user to specify the request locale as macro value. */
    public static final String KEY_REQUEST_LOCALE = "request.locale";

    /** The prefix indicating that the key represents a HTTP request parameter. */
    public static final String KEY_REQUEST_PARAM = "param.";

    /** Key used to specify the request uri as macro value. */
    public static final String KEY_REQUEST_URI = "request.uri";

    /** Key used to specify the validation path as macro value. */
    public static final String KEY_VALIDATION_PATH = "validation.path";

    /** Key used to specify the validation regex as macro value. */
    public static final String KEY_VALIDATION_REGEX = "validation.regex";

    /** Key used to specify the validation value as macro value. */
    public static final String KEY_VALIDATION_VALUE = "validation.value";

    /** Identified for "magic" parameter commands. */
    static final String[] VALUE_NAMES_ARRAY = {"uri", // 0
        "filename", // 1  
        "folder", // 2 
        "default.encoding", // 3  
        "remoteaddress", // 4
        "webapp", // 5
        "webbasepath", // 6 
        "version", // 7
        "element", // 8
        "sitemap" // 9
    };

    /** The "magic" commands wrapped in a List. */
    public static final List<String> VALUE_NAMES = Collections.unmodifiableList(Arrays.asList(VALUE_NAMES_ARRAY));

    /** The log object for this class. */
    private static final Log LOG = CmsLog.getLog(CmsMacroResolver.class);

    /** A map of additional values provided by the calling class. */
    protected Map<String, String> m_additionalMacros;

    /** The OpenCms user context to use for resolving macros. */
    protected CmsObject m_cms;

    /** The JSP's page context to use for resolving macros. */
    protected PageContext m_jspPageContext;

    /** Indicates if unresolved macros should be kept "as is" or replaced by an empty String. */
    protected boolean m_keepEmptyMacros;

    /** The messages resource bundle to resolve localized keys with. */
    protected CmsMessages m_messages;

    /** The resource name to use for resolving macros. */
    protected String m_resourceName;

    /**
     * Adds macro delimiters to the given input, 
     * for example <code>key</code> becomes <code>%(key)</code>.<p>
     * 
     * @param input the input to format as a macro
     * 
     * @return the input formatted as a macro
     */
    public static String formatMacro(String input) {

        StringBuffer result = new StringBuffer(input.length() + 4);
        result.append(I_CmsMacroResolver.MACRO_DELIMITER);
        result.append(I_CmsMacroResolver.MACRO_START);
        result.append(input);
        result.append(I_CmsMacroResolver.MACRO_END);
        return result.toString();
    }

    /**
     * Returns <code>true</code> if the given input String if formatted like a macro,
     * that is it starts with <code>{@link I_CmsMacroResolver#MACRO_DELIMITER_OLD} +
     * {@link I_CmsMacroResolver#MACRO_START_OLD}</code> and ends with 
     * <code>{@link I_CmsMacroResolver#MACRO_END_OLD}</code>.<p>
     * 
     * @param input the input to check for a macro
     * @return <code>true</code> if the given input String if formatted like a macro
     */
    public static boolean isMacro(String input) {

        if (CmsStringUtil.isEmpty(input) || (input.length() < 3)) {
            return false;
        }

        return (((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER_OLD) && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START_OLD) && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END_OLD))) || ((input.charAt(0) == I_CmsMacroResolver.MACRO_DELIMITER) && ((input.charAt(1) == I_CmsMacroResolver.MACRO_START) && (input.charAt(input.length() - 1) == I_CmsMacroResolver.MACRO_END))));
    }

    /**
     * Returns <code>true</code> if the given input String is a macro equal to the given macro name.<p>
     * 
     * @param input the input to check for a macro
     * @param macroName the macro name to check for
     * 
     * @return <code>true</code> if the given input String is a macro equal to the given macro name
     */
    public static boolean isMacro(String input, String macroName) {

        if (isMacro(input)) {
            return input.substring(2, input.length() - 1).equals(macroName);
        }
        return false;
    }

    /**
     * Returns a macro for the given localization key with the given parameters.<p>
     * 
     * @param keyName the name of the localized key
     * @param params the optional parameter array
     * 
     * @return a macro for the given localization key with the given parameters
     */
    public static String localizedKeyMacro(String keyName, Object[] params) {

        String parameters = "";
        if ((params != null) && (params.length > 0)) {
            for (int i = 0; i < params.length; i++) {
                if (params[i] != null) {
                    parameters += "|" + params[i].toString();
                }
            }
        }
        return ""
            + I_CmsMacroResolver.MACRO_DELIMITER
            + I_CmsMacroResolver.MACRO_START
            + CmsMacroResolver.KEY_LOCALIZED_PREFIX
            + keyName
            + parameters
            + I_CmsMacroResolver.MACRO_END;
    }

    /**
     * Factory method to create a new {@link CmsMacroResolver} instance.<p>
     * 
     * @return a new instance of a {@link CmsMacroResolver}
     */
    public static CmsMacroResolver newInstance() {

        return new CmsMacroResolver();
    }

    /**
     * Resolves the macros in the given input using the provided parameters.<p>
     * 
     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given 
     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
     * 
     * If a macro is found that can not be mapped to a value by the given macro resolver,
     * it is left untouched in the input.<p>
     * 
     * @param input the input in which to resolve the macros
     * @param cms the OpenCms user context to use when resolving macros
     * @param messages the message resource bundle to use when resolving macros
     * 
     * @return the input with the macros resolved
     */
    public static String resolveMacros(String input, CmsObject cms, CmsMessages messages) {

        CmsMacroResolver resolver = new CmsMacroResolver();
        resolver.m_cms = cms;
        resolver.m_messages = messages;
        resolver.m_keepEmptyMacros = true;
        return resolver.resolveMacros(input);
    }

    /**
     * Resolves macros in the provided input String using the given macro resolver.<p>
     * 
     * A macro in the form <code>%(key)</code> or <code>${key}</code> in the content is replaced with it's assigned value
     * returned by the <code>{@link I_CmsMacroResolver#getMacroValue(String)}</code> method of the given 
     * <code>{@link I_CmsMacroResolver}</code> instance.<p>
     * 
     * If a macro is found that can not be mapped to a value by the given macro resolver,
     * <code>{@link I_CmsMacroResolver#isKeepEmptyMacros()}</code> controls if the macro is replaced by
     * an empty String, or is left untouched in the input.<p>
     * 
     * @param input the input in which to resolve the macros
     * @param resolver the macro resolver to use
     * 
     * @return the input with all macros resolved
     */
    public static String resolveMacros(final String input, I_CmsMacroResolver resolver) {

        if ((input == null) || (input.length() < 3)) {
            // macro must have at last 3 chars "${}" or "%()"
            return input;
        }

        int pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER);
        int po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD);

        if ((po == -1) && (pn == -1)) {
            // no macro delimiter found in input
            return input;
        }

        int len = input.length();
        StringBuffer result = new StringBuffer(len << 1);
        int np, pp1, pp2, e;
        String macro, value;
        boolean keep = resolver.isKeepEmptyMacros();
        boolean resolvedNone = true;
        char ds, de;
        int p;

        if ((po == -1) || ((pn > -1) && (pn < po))) {
            p = pn;
            ds = I_CmsMacroResolver.MACRO_START;
            de = I_CmsMacroResolver.MACRO_END;
        } else {
            p = po;
            ds = I_CmsMacroResolver.MACRO_START_OLD;
            de = I_CmsMacroResolver.MACRO_END_OLD;
        }

        // append chars before the first delimiter found
        result.append(input.substring(0, p));
        do {
            pp1 = p + 1;
            pp2 = pp1 + 1;
            if (pp2 >= len) {
                // remaining chars can't be a macro (minimum size is 3)
                result.append(input.substring(p, len));
                break;
            }
            // get the next macro delimiter
            if ((pn > -1) && (pn < pp1)) {
                pn = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER, pp1);
            }
            if ((po > -1) && (po < pp1)) {
                po = input.indexOf(I_CmsMacroResolver.MACRO_DELIMITER_OLD, pp1);
            }
            if ((po == -1) && (pn == -1)) {
                // none found, make sure remaining chars in this segment are appended
                np = len;
            } else {
                // check if the next delimiter is old or new style
                if ((po == -1) || ((pn > -1) && (pn < po))) {
                    np = pn;
                } else {
                    np = po;
                }
            }
            // check if the next char is a "macro start"
            char st = input.charAt(pp1);
            if (st == ds) {
                // we have a starting macro sequence "${" or "%(", now check if this segment contains a "}" or ")"
                e = input.indexOf(de, p);
                if ((e > 0) && (e < np)) {
                    // this segment contains a closing macro delimiter "}" or "]", so we may have found a macro
                    macro = input.substring(pp2, e);
                    // resolve macro
                    value = resolver.getMacroValue(macro);
                    e++;
                    if (value != null) {
                        // macro was successfully resolved
                        result.append(value);
                        resolvedNone = false;
                    } else if (keep) {
                        // macro was unknown, but should be kept
                        result.append(input.substring(p, e));
                    }
                } else {
                    // no complete macro "${...}" or "%(...)" in this segment
                    e = p;
                }
            } else {
                // no macro start char after the "$" or "%"
                e = p;
            }
            // set macro style for next delimiter found
            if (np == pn) {
                ds = I_CmsMacroResolver.MACRO_START;
                de = I_CmsMacroResolver.MACRO_END;
            } else {
                ds = I_CmsMacroResolver.MACRO_START_OLD;
                de = I_CmsMacroResolver.MACRO_END_OLD;
            }
            // append the remaining chars after the macro to the start of the next macro
            result.append(input.substring(e, np));
            // this is a nerdy joke ;-)
            p = np;
        } while (p < len);

        if (resolvedNone && keep) {
            // nothing was resolved and macros should be kept, return original input
            return input;
        }

        // input was changed during resolving of macros
        return result.toString();
    }

    /**
     * Strips the macro delimiters from the given input, 
     * for example <code>%(key)</code> or <code>${key}</code> becomes <code>key</code>.<p>
     * 
     * In case the input is not a macro, <code>null</code> is returned.<p>
     * 
     * @param input the input to strip
     * 
     * @return the macro stripped from the input, or <code>null</code>
     */
    public static String stripMacro(String input) {

        if (isMacro(input)) {
            return input.substring(2, input.length() - 1);
        }
        return null;
    }

    /**
     * Adds a customized macro to this macro resolver.<p>
     * 
     * @param key the macro to add
     * @param value the value to return if the macro is encountered
     */
    public void addMacro(String key, String value) {

        if (m_additionalMacros == null) {
            // use lazy initializing
            m_additionalMacros = new HashMap<String, String>();
        }
        m_additionalMacros.put(key, value);
    }

    /**
     * @see org.opencms.util.I_CmsMacroResolver#getMacroValue(java.lang.String)
     */
    public String getMacroValue(String macro) {

        if (m_messages != null) {
            if (macro.startsWith(CmsMacroResolver.KEY_LOCALIZED_PREFIX)) {
                String keyName = macro.substring(CmsMacroResolver.KEY_LOCALIZED_PREFIX.length());
                return m_messages.keyWithParams(keyName);
            }
        }

        if (m_jspPageContext != null) {

            if (m_jspPageContext.getRequest() != null) {
                if (macro.startsWith(CmsMacroResolver.KEY_REQUEST_PARAM)) {
                    // the key is a request parameter  
                    macro = macro.substring(CmsMacroResolver.KEY_REQUEST_PARAM.length());
                    String result = m_jspPageContext.getRequest().getParameter(macro);
                    if ((result == null) && macro.equals(KEY_PROJECT_ID)) {
                        result = m_cms.getRequestContext().currentProject().getUuid().toString();
                    }
                    return result;
                }

                if ((m_cms != null) && macro.startsWith(CmsMacroResolver.KEY_PROPERTY_ELEMENT)) {

                    // the key is a cms property to be read on the current element

                    macro = macro.substring(CmsMacroResolver.KEY_PROPERTY_ELEMENT.length());
                    CmsFlexController controller = CmsFlexController.getController(m_jspPageContext.getRequest());
                    try {
                        CmsProperty property = m_cms.readPropertyObject(
                            controller.getCurrentRequest().getElementUri(),
                            macro,
                            false);
                        if (property != CmsProperty.getNullProperty()) {
                            return property.getValue();
                        }
                    } catch (CmsException e) {
                        if (LOG.isWarnEnabled()) {
                            LOG.warn(Messages.get().getBundle().key(
                                Messages.LOG_PROPERTY_READING_FAILED_2,
                                macro,
                                controller.getCurrentRequest().getElementUri()), e);
                        }
                    }
                }
            }

            if (macro.startsWith(CmsMacroResolver.KEY_PAGE_CONTEXT)) {
                // the key is a page context object
                macro = macro.substring(CmsMacroResolver.KEY_PAGE_CONTEXT.length());
                int scope = m_jspPageContext.getAttributesScope(macro);
                return m_jspPageContext.getAttribute(macro, scope).toString();
            }
        }

        if (m_cms != null) {

            if (macro.startsWith(CmsMacroResolver.KEY_PROPERTY)) {
                // the key is a cms property to be read on the current request URI
                macro = macro.substring(CmsMacroResolver.KEY_PROPERTY.length());
                try {
                    CmsProperty property = m_cms.readPropertyObject(m_cms.getRequestContext().getUri(), macro, true);
                    if (property != CmsProperty.getNullProperty()) {
                        return property.getValue();
                    }
                } catch (CmsException e) {
                    if (LOG.isWarnEnabled()) {
                        CmsMessageContainer message = Messages.get().container(
                            Messages.LOG_PROPERTY_READING_FAILED_2,
                            macro,
                            m_cms.getRequestContext().getUri());
                        LOG.warn(message.key(), e);
                    }
                }
                return null;
            }

            if (macro.startsWith(CmsMacroResolver.KEY_ATTRIBUTE)) {
                // the key is an OpenCms runtime attribute
                macro = macro.substring(CmsMacroResolver.KEY_ATTRIBUTE.length());
                Object attribute = m_cms.getRequestContext().getAttribute(macro);
                if (attribute != null) {
                    return attribute.toString();
                }
                return null;
            }

            if (macro.startsWith(CmsMacroResolver.KEY_OPENCMS)) {

                // the key is a shortcut for a cms runtime value

                String originalKey = macro;
                macro = macro.substring(CmsMacroResolver.KEY_OPENCMS.length());
                int index = VALUE_NAMES.indexOf(macro);
                String value = null;

                switch (index) {
                    case 0:
                        // "uri"
                        value = m_cms.getRequestContext().getUri();
                        break;
                    case 1:
                        // "filename"
                        value = m_resourceName;
                        break;
                    case 2:
                        // folder
                        value = m_cms.getRequestContext().getFolderUri();
                        break;
                    case 3:
                        // default.encoding
                        value = OpenCms.getSystemInfo().getDefaultEncoding();
                        break;
                    case 4:
                        // remoteaddress
                        value = m_cms.getRequestContext().getRemoteAddress();
                        break;
                    case 5:
                        // webapp
                        value = OpenCms.getSystemInfo().getWebApplicationName();
                        break;
                    case 6:
                        // webbasepath
                        value = OpenCms.getSystemInfo().getWebApplicationRfsPath();
                        break;
                    case 7:
                        // version
                        value = OpenCms.getSystemInfo().getVersionNumber();
                        break;
                    case 8:
                        // element
                        if (m_jspPageContext != null) {
                            try {
                                CmsContainerElementBean element = OpenCms.getADEManager().getCurrentElement(
                                    m_jspPageContext.getRequest());
                                value = element.getSitePath();
                            } catch (CmsException e) {
                                if (!LOG.isDebugEnabled()) {
                                    LOG.warn(e.getLocalizedMessage());
                                }
                                LOG.debug(e.getLocalizedMessage(), e);
                            }
                        }
                        break;
                    case 9:
                        // sitemap
                        CmsSiteEntryBean sitemap = OpenCms.getSitemapManager().getRuntimeInfo(
                            m_jspPageContext.getRequest());
                        value = m_cms.getRequestContext().getUri();
                        if (sitemap != null) {
                            value = sitemap.getUri();
                        }
                        break;
                    default:
                        // return the key "as is"
                        value = originalKey;
                        break;
                }

                return value;
            }
{'fraction_non_alphanumeric': 0.09272046510752789, 'fraction_numerical': 0.004402716890251933, 'mean_word_length': 1.8402714690322235, 'pattern_counts': {'":': 0, '<': 177, '<?xml version=': 0, '>': 169, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3214272', 'n_tokens_mistral': 14497, 'n_tokens_neox': 13705, 'n_words': 4446}