/*
 * 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.struts2.components;

import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
import org.apache.struts2.components.template.TemplateEngineManager;
import org.apache.struts2.components.template.TemplateRenderingContext;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.util.ComponentUtils;
import org.apache.struts2.util.TextProviderHelper;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
import org.apache.struts2.views.util.ContextUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Writer;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * <p>
 * UIBean is the standard superclass of all Struts UI components.
 * It defines common Struts and html properties all UI components should present for usage.
 * </p>
 * <!-- START SNIPPET: templateRelatedAttributes -->
 *
 * <table border="1" summary="">
 *    <thead>
 *       <tr>
 *          <td>Attribute</td>
 *          <td>Theme</td>
 *          <td>Data Types</td>
 *          <td>Description</td>
 *       </tr>
 *    </thead>
 *    <tbody>
 *       <tr>
 *          <td>templateDir</td>
 *          <td>n/a</td>
 *          <td>String</td>
 *          <td>define the template directory</td>
 *       </tr>
 *       <tr>
 *          <td>theme</td>
 *          <td>n/a</td>
 *          <td>String</td>
 *          <td>define the theme name</td>
 *       </tr>
 *       <tr>
 *          <td>template</td>
 *          <td>n/a</td>
 *          <td>String</td>
 *          <td>define the template name</td>
 *       </tr>
 *       <tr>
 *          <td>themeExpansionToken</td>
 *          <td>n/a</td>
 *          <td>String</td>
 *          <td>special token (defined with struts.ui.theme.expansion.token) used to search for template in parent theme
 *          (don't use it separately!)</td>
 *       </tr>
 *       <tr>
 *          <td>expandTheme</td>
 *          <td>n/a</td>
 *          <td>String</td>
 *          <td>concatenation of themeExpansionToken and theme which tells internal template loader mechanism
 *          to try load template from current theme and then from parent theme (and parent theme, and so on)
 *          when used with &lt;#include/&gt; directive</td>
 *       </tr>
 *    </tbody>
 * </table>
 *
 * <!-- END SNIPPET: templateRelatedAttributes -->
 *
 * <!-- START SNIPPET: generalAttributes -->
 *
 * <table border="1" summary="">
 *    <thead>
 *       <tr>
 *          <td>Attribute</td>
 *          <td>Theme</td>
 *          <td>Data Types</td>
 *          <td>Description</td>
 *       </tr>
 *    </thead>
 *    <tbody>
 *       <tr>
 *          <td>cssClass</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>define html class attribute</td>
 *       </tr>
 *       <tr>
 *          <td>cssStyle</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>define html style attribute</td>
 *       </tr>
 *       <tr>
 *          <td>cssErrorClass</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>error class attribute</td>
 *       </tr>
 *       <tr>
 *          <td>cssErrorStyle</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>error style attribute</td>
 *       </tr>
 *       <tr>
 *          <td>title</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>define html title attribute</td>
 *       </tr>
 *       <tr>
 *          <td>disabled</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>define html disabled attribute</td>
 *       </tr>
 *       <tr>
 *          <td>label</td>
 *          <td>xhtml</td>
 *          <td>String</td>
 *          <td>define label of form element</td>
 *       </tr>
 *       <tr>
 *          <td>labelPosition</td>
 *          <td>xhtml</td>
 *          <td>String</td>
 *          <td>define label position of form element (top/left), default to left</td>
 *       </tr>
 *       <tr>
 *          <td>requiredPosition</td>
 *          <td>xhtml</td>
 *          <td>String</td>
 *          <td>define required label position of form element (left/right), default to right</td>
 *       </tr>
  *       <tr>
 *          <td>errorPosition</td>
 *          <td>xhtml</td>
 *          <td>String</td>
 *          <td>define error position of form element (top|bottom), default to top</td>
 *       </tr>
 *       <tr>
 *          <td>name</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>Form Element's field name mapping</td>
 *       </tr>
 *       <tr>
 *          <td>requiredLabel</td>
 *          <td>xhtml</td>
 *          <td>Boolean</td>
 *          <td>add * to label (true to add false otherwise)</td>
 *       </tr>
 *       <tr>
 *          <td>tabIndex</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>define html tabindex attribute</td>
 *       </tr>
 *       <tr>
 *          <td>value</td>
 *          <td>simple</td>
 *          <td>Object</td>
 *          <td>define value of form element</td>
 *       </tr>
 *    </tbody>
 * </table>
 *
 * <!-- END SNIPPET: generalAttributes -->
 *
 * <!-- START SNIPPET: javascriptRelatedAttributes -->
 *
 * <table border="1" summary="">
 *    <thead>
 *       <tr>
 *          <td>Attribute</td>
 *          <td>Theme</td>
 *          <td>Data Types</td>
 *          <td>Description</td>
 *       </tr>
 *    </thead>
 *    <tbody>
 *       <tr>
 *          <td>onclick</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onclick attribute</td>
 *       </tr>
 *       <tr>
 *          <td>ondblclick</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript ondbclick attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onmousedown</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onmousedown attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onmouseup</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onmouseup attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onmouseover</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onmouseover attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onmouseout</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onmouseout attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onfocus</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onfocus attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onblur</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onblur attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onkeypress</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onkeypress attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onkeyup</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onkeyup attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onkeydown</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onkeydown attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onselect</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onselect attribute</td>
 *       </tr>
 *       <tr>
 *          <td>onchange</td>
 *          <td>simple</td>
 *          <td>String</td>
 *          <td>html javascript onchange attribute</td>
 *       </tr>
 *    </tbody>
 * </table>
 *
 * <!-- END SNIPPET: javascriptRelatedAttributes -->
 *
 * <!-- START SNIPPET: tooltipattributes -->
 *
 * <table border="1" summary="">
 *  <tr>
 *     <td>Attribute</td>
 *     <td>Data Type</td>
 *     <td>Default</td>
 *     <td>Description</td>
 *  </tr>
 *  <tr>
 *      <td>tooltip</td>
 *      <td>String</td>
 *      <td>none</td>
 *      <td>Set the tooltip of this particular component</td>
 *  </tr>
 *  <tr>
 *      <td>jsTooltipEnabled</td>
 *      <td>String</td>
 *      <td>false</td>
 *      <td>Enable js tooltip rendering</td>
 *  </tr>
 *    <tr>
 *      <td>tooltipIcon</td>
 *      <td>String</td>
 *      <td>/static/tooltip/tooltip.gif</td>
 *      <td>The url to the tooltip icon</td>
 *   <tr>
 *      <td>tooltipDelay</td>
 *      <td>String</td>
 *      <td>500</td>
 *      <td>Tooltip shows up after the specified timeout (miliseconds). A behavior similar to that of OS based tooltips.</td>
 *   </tr>
 *   <tr>
 *      <td>key</td>
 *      <td>simple</td>
 *      <td>String</td>
 *      <td>The name of the property this input field represents.  This will auto populate the name, label, and value</td>
 *   </tr>
 * </table>
 *
 * <!-- END SNIPPET: tooltipattributes -->
 *
 *
 * <!-- START SNIPPET: tooltipdescription -->
 * <p>
 * <b>tooltipConfig is deprecated, use individual tooltip configuration attributes instead </b>
 * </p>
 *
 * <p>
 * Every Form UI component (in xhtml / css_xhtml or any other that extends them) can
 * have tooltips assigned to them. The Form component's tooltip related attribute, once
 * defined, will be applied to all form UI components that are created under it unless
 * explicitly overridden by having the Form UI component itself defined with their own tooltip attribute.
 * </p>
 *
 * <p>
 * In Example 1, the textfield will inherit the tooltipDelay and tooltipIconPath attribute from
 * its containing form. In other words, although it doesn't define a tooltipIconPath
 * attribute, it will have that attribute inherited from its containing form.
 * </p>
 *
 * <p>
 * In Example 2, the  textfield will inherit both the tooltipDelay and
 * tooltipIconPath attribute from its containing form, but the tooltipDelay
 * attribute is overridden at the textfield itself. Hence, the textfield actually will
 * have its tooltipIcon defined as /myImages/myIcon.gif, inherited from its containing form, and
 * tooltipDelay defined as 5000.
 * </p>
 *
 * <p>
 * Example 3, 4 and 5 show different ways of setting the tooltip configuration attribute.<br>
 * <b>Example 3:</b> Set tooltip config through the body of the param tag<br>
 * <b>Example 4:</b> Set tooltip config through the value attribute of the param tag<br>
 * <b>Example 5:</b> Set tooltip config through the tooltip attributes of the component tag<br>
 * </p>
 *
 * <!-- END SNIPPET: tooltipdescription -->
 *
 *
 * <pre>
 * <!-- START SNIPPET: tooltipexample -->
 *
 * &lt;!-- Example 1: --&gt;
 * &lt;s:form
 *          tooltipDelay="500"
 *          tooltipIconPath="/myImages/myIcon.gif" .... &gt;
 *   ....
 *     &lt;s:textfield label="Customer Name" tooltip="Enter the customer name" .... /&gt;
 *   ....
 * &lt;/s:form&gt;
 *
 * &lt;!-- Example 2: --&gt;
 * &lt;s:form
 *          tooltipDelay="500"
 *          tooltipIconPath="/myImages/myIcon.gif" .... &gt;
 *   ....
 *     &lt;s:textfield label="Address"
 *          tooltip="Enter your address"
 *          tooltipDelay="5000" /&gt;
 *   ....
 * &lt;/s:form&gt;
 *
 *
 * &lt;-- Example 3: --&gt;
 * &lt;s:textfield
 *        label="Customer Name"
 *        tooltip="One of our customer Details"&gt;
 *        &lt;s:param name="tooltipDelay"&gt;
 *             500
 *        &lt;/s:param&gt;
 *        &lt;s:param name="tooltipIconPath"&gt;
 *             /myImages/myIcon.gif
 *        &lt;/s:param&gt;
 * &lt;/s:textfield&gt;
 *
 *
 * &lt;-- Example 4: --&gt;
 * &lt;s:textfield
 *          label="Customer Address"
 *          tooltip="Enter The Customer Address" &gt;
 *          &lt;s:param
 *              name="tooltipDelay"
 *              value="500" /&gt;
 * &lt;/s:textfield&gt;
 *
 *
 * &lt;-- Example 5: --&gt;
 * &lt;s:textfield
 *          label="Customer Telephone Number"
 *          tooltip="Enter customer Telephone Number"
 *          tooltipDelay="500"
 *          tooltipIconPath="/myImages/myIcon.gif" /&gt;
 *
 * <!-- END SNIPPET: tooltipexample -->
 * </pre>
 *
 */
public abstract class UIBean extends Component {

    private static final Logger LOG = LogManager.getLogger(UIBean.class);

    protected static final String ATTR_FIELD_VALUE = "fieldValue";
    protected static final String ATTR_NAME_VALUE = "nameValue";
    protected static final String ATTR_VALUE = "value";

    protected HttpServletRequest request;
    protected HttpServletResponse response;

    public UIBean(ValueStack stack, HttpServletRequest request, HttpServletResponse response) {
        super(stack);
        this.request = request;
        this.response = response;
        this.templateSuffix = ContextUtil.getTemplateSuffix(stack.getContext());
    }

    // The templateSuffic to use, overrides the default one if not null.
    protected String templateSuffix;

    // The template to use, overrides the default one.
    protected String template;

    // templateDir and theme attributes
    protected String templateDir;
    protected String theme;

    // shortcut, sets label, name, and value
    protected String key;

    protected String id;
    protected String cssClass;
    protected String cssStyle;
    protected String cssErrorClass;
    protected String cssErrorStyle;
    protected String disabled;
    protected String label;
    protected String labelPosition;
    protected String labelSeparator;
    protected String requiredPosition;
    protected String errorPosition;
    protected String name;
    protected String requiredLabel;
    protected String tabindex;
    protected String value;
    protected String title;

    // HTML scripting events attributes
    protected String onclick;
    protected String ondblclick;
    protected String onmousedown;
    protected String onmouseup;
    protected String onmouseover;
    protected String onmousemove;
    protected String onmouseout;
    protected String onfocus;
    protected String onblur;
    protected String onkeypress;
    protected String onkeydown;
    protected String onkeyup;
    protected String onselect;
    protected String onchange;

    // common html attributes
    protected String accesskey;

    // javascript tooltip attribute
    protected String tooltip;
    protected String tooltipConfig;
    protected String javascriptTooltip;
    protected String tooltipDelay;
    protected String tooltipCssClass;
    protected String tooltipIconPath;

    // dynamic attributes
    protected Map<String, Object> dynamicAttributes = new HashMap<>();

    protected String defaultTemplateDir;
    protected String defaultUITheme;
    protected String uiThemeExpansionToken;
    protected String uiStaticContentPath;

    protected TemplateEngineManager templateEngineManager;

    @Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
    public void setDefaultTemplateDir(String dir) {
        this.defaultTemplateDir = dir;
    }

    @Inject(StrutsConstants.STRUTS_UI_THEME)
    public void setDefaultUITheme(String theme) {
        this.defaultUITheme = theme;
    }

    @Inject(StrutsConstants.STRUTS_UI_THEME_EXPANSION_TOKEN)
    public void setUIThemeExpansionToken(String uiThemeExpansionToken) {
        this.uiThemeExpansionToken = uiThemeExpansionToken;
    }

    @Inject(StrutsConstants.STRUTS_UI_STATIC_CONTENT_PATH)
    public void setStaticContentPath(String uiStaticContentPath) {
        this.uiStaticContentPath = StaticContentLoader.Validator.validateStaticContentPath(uiStaticContentPath);
    }

    @Inject
    public void setTemplateEngineManager(TemplateEngineManager mgr) {
        this.templateEngineManager = mgr;
    }

    @Override
    public boolean end(Writer writer, String body) {
        evaluateParams();
        try {
            super.end(writer, body, false);
            mergeTemplate(writer, buildTemplateName(template, getDefaultTemplate()));
        } catch (Exception e) {
            throw new StrutsException(e);
        }
        finally {
            popComponentStack();
        }

        return false;
    }

    /**
     * A contract that requires each concrete UI Tag to specify which template should be used as a default.  For
     * example, the CheckboxTab might return "checkbox.vm" while the RadioTag might return "radio.vm".  This value
     * <strong>not</strong> begin with a '/' unless you intend to make the path absolute rather than relative to the
     * current theme.
     *
     * @return The name of the template to be used as the default.
     */
    protected abstract String getDefaultTemplate();

    protected Template buildTemplateName(String myTemplate, String myDefaultTemplate) {
        String templateName = myDefaultTemplate;

        if (myTemplate != null) {
            templateName = findString(myTemplate);
        }

        return new Template(getTemplateDir(), getTheme(), templateName);

    }

    protected void mergeTemplate(Writer writer, Template template) throws Exception {
        final TemplateEngine engine = templateEngineManager.getTemplateEngine(template, templateSuffix);
        if (engine == null) {
            throw new ConfigurationException("Unable to find a TemplateEngine for template " + template);
        }

        LOG.debug("Rendering template {}", template);

        final TemplateRenderingContext context = new TemplateRenderingContext(template, writer, getStack(), getParameters(), this);
        engine.renderTemplate(context);
    }

    public String getTemplateDir() {
        String result = null;

        if (this.templateDir != null) {
            result = findString(this.templateDir);
        }

        // Default template set
        if (StringUtils.isBlank(result)) {
            result = defaultTemplateDir;
        }

        // Defaults to 'template'
        if (StringUtils.isBlank(result)) {
            result = "template";
        }

        return result;
    }

    public String getTheme() {
        String result = null;

        if (this.theme != null) {
            result = findString(this.theme);
        }

        if (StringUtils.isBlank(result)) {
            Form form = (Form) findAncestor(Form.class);
            if (form != null) {
                result = form.getTheme();
            }
        }

        // Default theme set
        if (StringUtils.isBlank(result)) {
            result = defaultUITheme;
        }

        return result;
    }

    public void evaluateParams() {
        String gotTheme = getTheme();

        addParameter("templateDir", getTemplateDir());
        addParameter("theme", gotTheme);
        addParameter("template", template != null ? findString(template) : getDefaultTemplate());
        addParameter("dynamicAttributes", dynamicAttributes);
        addParameter("themeExpansionToken", uiThemeExpansionToken);
        addParameter("expandTheme", uiThemeExpansionToken + gotTheme);

        addParameter("staticContentPath", findString(uiStaticContentPath));

        String translatedName = null;
        String providedLabel = null;

        if (this.key != null) {

            if(this.name == null) {
                this.name = key;
            }

            if(this.label == null) {
                // lookup the label from a TextProvider (default value is the key)
                providedLabel = TextProviderHelper.getText(key, key, stack);
            }
        }

        if (this.name != null) {
            translatedName = findString(this.name);
            addParameter("name", translatedName);
        }

        if (label != null) {
            addParameter("label", findString(label));
        } else {
            if (providedLabel != null) {
                // label found via a TextProvider
                addParameter("label", providedLabel);
            }
        }

        if (labelSeparator != null) {
            addParameter("labelseparator", findString(labelSeparator));
        }

        if (labelPosition != null) {
            String labelPosition = findString(this.labelPosition);
            addParameter("labelPosition", labelPosition);
        }

        if (requiredPosition != null) {
            addParameter("requiredPosition", findString(requiredPosition));
        }

        if (errorPosition != null) {
            addParameter("errorposition", findString(errorPosition));
        }

        if (requiredLabel != null) {
            Object parsedValue = findValue(requiredLabel, Boolean.class);
            addParameter("required", parsedValue == null ? Boolean.valueOf(requiredLabel) : parsedValue);
        }

        if (disabled != null) {
            Object parsedValue = findValue(disabled, Boolean.class);
            addParameter("disabled", parsedValue == null ? Boolean.valueOf(disabled) : parsedValue);
        }

        if (tabindex != null) {
            addParameter("tabindex", findString(tabindex));
        }

        if (onclick != null) {
            addParameter("onclick", findString(onclick));
        }

        if (ondblclick != null) {
            addParameter("ondblclick", findString(ondblclick));
        }

        if (onmousedown != null) {
            addParameter("onmousedown", findString(onmousedown));
        }

        if (onmouseup != null) {
            addParameter("onmouseup", findString(onmouseup));
        }

        if (onmouseover != null) {
            addParameter("onmouseover", findString(onmouseover));
        }

        if (onmousemove != null) {
            addParameter("onmousemove", findString(onmousemove));
        }

        if (onmouseout != null) {
            addParameter("onmouseout", findString(onmouseout));
        }

        if (onfocus != null) {
            addParameter("onfocus", findString(onfocus));
        }

        if (onblur != null) {
            addParameter("onblur", findString(onblur));
        }

        if (onkeypress != null) {
            addParameter("onkeypress", findString(onkeypress));
        }

        if (onkeydown != null) {
            addParameter("onkeydown", findString(onkeydown));
        }

        if (onkeyup != null) {
            addParameter("onkeyup", findString(onkeyup));
        }

        if (onselect != null) {
            addParameter("onselect", findString(onselect));
        }

        if (onchange != null) {
            addParameter("onchange", findString(onchange));
        }

        if (accesskey != null) {
            addParameter("accesskey", findString(accesskey));
        }

        if (cssClass != null) {
            addParameter("cssClass", findString(cssClass));
        }

        if (cssStyle != null) {
            addParameter("cssStyle", findString(cssStyle));
        }

        if (cssErrorClass != null) {
            addParameter("cssErrorClass", findString(cssErrorClass));
        }

        if (cssErrorStyle != null) {
            addParameter("cssErrorStyle", findString(cssErrorStyle));
        }

        if (title != null) {
            addParameter("title", findString(title));
        }

        applyValueParameter(translatedName);

        final Form form = (Form) findAncestor(Form.class);

        // create HTML id element
        populateComponentHtmlId(form);

        if (form != null ) {
            addParameter("form", form.getParameters());

            if ( translatedName != null ) {
                // list should have been created by the form component
                List<String> tags = (List<String>) form.getParameters().get("tagNames");
                tags.add(translatedName);
            }
        }


        // tooltip & tooltipConfig
        if (tooltipConfig != null) {
            addParameter("tooltipConfig", findValue(tooltipConfig));
        }
        if (tooltip != null) {
            addParameter("tooltip", findString(tooltip));

            Map<String, String> tooltipConfigMap = getTooltipConfig(this);

            if (form != null) { // inform the containing form that we need tooltip javascript included
                form.addParameter("hasTooltip", Boolean.TRUE);

                // tooltipConfig defined in component itself will take precedence
                // over those defined in the containing form
                Map<String, String> overallTooltipConfigMap = getTooltipConfig(form);
                overallTooltipConfigMap.putAll(tooltipConfigMap); // override parent form's tooltip config

                for (Map.Entry<String, String> entry : overallTooltipConfigMap.entrySet()) {
                    addParameter(entry.getKey(), entry.getValue());
                }
            }
            else {
                LOG.warn("No ancestor Form found, javascript based tooltip will not work, however standard HTML tooltip using alt and title attribute will still work");
            }

            //TODO: this is to keep backward compatibility, remove once when tooltipConfig is dropped
            String  jsTooltipEnabled = (String) getParameters().get("jsTooltipEnabled");
            if (jsTooltipEnabled != null)
                this.javascriptTooltip = jsTooltipEnabled;

            //TODO: this is to keep backward compatibility, remove once when tooltipConfig is dropped
            String tooltipIcon = (String) getParameters().get("tooltipIcon");
            if (tooltipIcon != null)
                this.addParameter("tooltipIconPath", tooltipIcon);
            if (this.tooltipIconPath != null)
                this.addParameter("tooltipIconPath", findString(this.tooltipIconPath));

            //TODO: this is to keep backward compatibility, remove once when tooltipConfig is dropped
            String tooltipDelayParam = (String) getParameters().get("tooltipDelay");
            if (tooltipDelayParam != null)
                this.addParameter("tooltipDelay", tooltipDelayParam);
            if (this.tooltipDelay != null)
                this.addParameter("tooltipDelay", findString(this.tooltipDelay));

            if (this.javascriptTooltip != null) {
                Object jsTooltips = findValue(this.javascriptTooltip, Boolean.class);
                //TODO use a Boolean model when tooltipConfig is dropped
                this.addParameter("jsTooltipEnabled", jsTooltips == null ? this.javascriptTooltip : jsTooltips.toString());

                if (form != null)
                    form.addParameter("hasTooltip", jsTooltips);
                if (this.tooltipCssClass != null)
                    this.addParameter("tooltipCssClass", findString(this.tooltipCssClass));
            }
        }

        // to be used with the CSP interceptor - adds the nonce value as a parameter to be accessed from ftl files
        Map<String, Object> session = stack.getActionContext().getSession();
        Object nonceValue = session != null ? session.get("nonce") : null;
        if (nonceValue != null) {
            addParameter("nonce", nonceValue.toString());
        }

        evaluateExtraParams();
    }

    /**
     * Tries to calculate the "value" parameter based either on the provided {@link #value} or {@link #name}
     * @param translatedName the already evaluated {@link #name}
     */
    protected void applyValueParameter(String translatedName) {
        // see if the value has been specified as a parameter already
        if (parameters.containsKey(ATTR_VALUE)) {
            parameters.put(ATTR_NAME_VALUE, parameters.get(ATTR_VALUE));
        } else {
            if (evaluateNameValue()) {
                final Class<?> valueClazz = getValueClassType();

                if (valueClazz != null) {
                    if (value != null) {
                        addParameter(ATTR_NAME_VALUE, findValue(value, valueClazz));
                    } else if (translatedName != null) {
                        processTranslatedName(translatedName, (expr) -> findValue(expr, valueClazz));
                    }
                } else {
                    if (value != null) {
                        addParameter(ATTR_NAME_VALUE, findValue(value));
                    } else if (translatedName != null) {
                        processTranslatedName(translatedName, this::findValue);
                    }
                }
            }
        }
    }

    private void processTranslatedName(String translatedName, Function<String, Object> evaluator) {
        boolean evaluated = !translatedName.equals(this.name);
        boolean reevaluate = !evaluated || isAcceptableExpression(translatedName);
        if (!reevaluate) {
            addParameter(ATTR_NAME_VALUE, translatedName);
        } else {
            String expr = completeExpression(translatedName);
            addParameter(ATTR_NAME_VALUE, evaluator.apply(expr));
        }
    }

    protected String escape(String name) {
        // escape any possible values that can make the ID painful to work with in JavaScript
        if (name != null) {
            return name.replaceAll("[^a-zA-Z0-9_]", "_");
        } else {
            return null;
        }
    }

    /**
     * Ensures an unescaped attribute value cannot be vulnerable to XSS attacks
     *
     * @param val The value to check
     * @return The escaped value
     */
    protected String ensureAttributeSafelyNotEscaped(String val) {
        if (val != null) {
            return val.replaceAll("\"", "&#34;");
        } else {
            return null;
        }
    }

    protected void evaluateExtraParams() {
    }

    protected boolean evaluateNameValue() {
        return true;
    }

    protected Class<?> getValueClassType() {
        return String.class;
    }

    public void addFormParameter(String key, Object value) {
        Form form = (Form) findAncestor(Form.class);
        if (form != null) {
            form.addParameter(key, value);
        }
    }

    protected void enableAncestorFormCustomOnsubmit() {
        Form form = (Form) findAncestor(Form.class);
        if (form != null) {
            form.addParameter("customOnsubmitEnabled", Boolean.TRUE);
        } else {
            if (LOG.isWarnEnabled()) {
                LOG.warn("Cannot find an Ancestor form, custom onsubmit is NOT enabled");
            }
        }
    }

    protected Map<String, String> getTooltipConfig(UIBean component) {
        Object tooltipConfigObj = component.getParameters().get("tooltipConfig");
        Map<String, String> result = new LinkedHashMap<>();

        if (tooltipConfigObj instanceof Map) {
            // we get this if its configured using
            // 1] UI component's tooltipConfig attribute  OR
            // 2] <param name="tooltip" value="" /> param tag value attribute

            result = new LinkedHashMap<String, String>((Map) tooltipConfigObj);
        } else if (tooltipConfigObj instanceof String) {

            // we get this if its configured using
            // <param name="tooltipConfig"> ... </param> tag's body
            String tooltipConfigStr = (String) tooltipConfigObj;
            String[] tooltipConfigArray = tooltipConfigStr.split("\\|");

            for (String aTooltipConfigArray : tooltipConfigArray) {
                String[] configEntry = aTooltipConfigArray.trim().split("=");
                String configKey = configEntry[0].trim();
                String configValue;
                if (configEntry.length > 1) {
                    configValue = configEntry[1].trim();
                    result.put(configKey, configValue);
                } else {
                    LOG.warn("component {} tooltip config param {} has no value defined, skipped", component, configKey);
                }
            }
        }
        if (component.javascriptTooltip != null)
            result.put("jsTooltipEnabled", component.javascriptTooltip);
        if (component.tooltipIconPath != null)
            result.put("tooltipIcon", component.tooltipIconPath);
        if (component.tooltipDelay != null)
            result.put("tooltipDelay", component.tooltipDelay);
        return result;
    }

    /**
     * Create HTML id element for the component and populate this component parameter
     * map. Additionally, a parameter named escapedId is populated which contains the found id value filtered by
     * {@link #escape(String)}, needed eg. for naming Javascript identifiers based on the id value.
     *
     * The order is as follows :-
     * <ol>
     *   <li>This component id attribute</li>
     *   <li>[containing_form_id]_[this_component_name]</li>
     *   <li>[this_component_name]</li>
     * </ol>
     *
     * @param form enclosing form tag
     */
    protected void populateComponentHtmlId(Form form) {
        String tryId;
        String generatedId;
        if (id != null) {
            // this check is needed for backwards compatibility with 2.1.x
            tryId = findString(id);
        } else if (null == (generatedId = escape(name != null ? findString(name) : null))) {
            LOG.debug("Cannot determine id attribute for [{}], consider defining id, name or key attribute!", this);
            tryId = null;
        } else if (form != null) {
            tryId = form.getParameters().get("id") + "_" + generatedId;
        } else {
            tryId = generatedId;
        }

        //fix for https://issues.apache.org/jira/browse/WW-4299
        //do not assign value to id if tryId is null
        if (tryId != null) {
          addParameter("id", tryId);
          addParameter("escapedId", escape(tryId));
        }
    }

    /**
     * Get's the id for referencing element.
     * @return the id for referencing element.
     */
    public String getId() {
        return id;
    }

    @StrutsTagAttribute(description="HTML id attribute")
    public void setId(String id) {
        this.id = id;
    }

    @StrutsTagAttribute(description="The template directory.")
    public void setTemplateDir(String templateDir) {
        this.templateDir = templateDir;
    }

    @StrutsTagAttribute(description="The theme (other than default) to use for rendering the element")
    public void setTheme(String theme) {
        this.theme = theme;
    }

    public String getTemplate() {
        return template;
    }

    @StrutsTagAttribute(description="The template (other than default) to use for rendering the element")
    public void setTemplate(String template) {
        this.template = template;
    }

    @StrutsTagAttribute(description="The css class to use for element")
    public void setCssClass(String cssClass) {
        this.cssClass = cssClass;
    }

    @StrutsTagAttribute(description="The css style definitions for element to use")
    public void setCssStyle(String cssStyle) {
        this.cssStyle = cssStyle;
    }

    @StrutsTagAttribute(description="The css style definitions for element to use - it's an alias of cssStyle attribute.")
    public void setStyle(String cssStyle) {
        this.cssStyle = cssStyle;
    }

    @StrutsTagAttribute(description="The css error class to use for element")
    public void setCssErrorClass(String cssErrorClass) {
        this.cssErrorClass = cssErrorClass;
    }

    @StrutsTagAttribute(description="The css error style definitions for element to use")
    public void setCssErrorStyle(String cssErrorStyle) {
        this.cssErrorStyle = cssErrorStyle;
    }

    @StrutsTagAttribute(description="Set the html title attribute on rendered html element")
    public void setTitle(String title) {
        this.title = title;
    }

    @StrutsTagAttribute(description="Set the html disabled attribute on rendered html element")
    public void setDisabled(String disabled) {
        this.disabled = disabled;
    }

    @StrutsTagAttribute(description="Label expression used for rendering an element specific label")
    public void setLabel(String label) {
        this.label = label;
    }

    @StrutsTagAttribute(description="String that will be appended to the label", defaultValue=":")
    public void setLabelSeparator(String labelseparator) {
        this.labelSeparator = labelseparator;
    }

    @StrutsTagAttribute(description="Define label position of form element (top/left)")
    public void setLabelPosition(String labelPosition) {
        this.labelPosition = labelPosition;
    }

    @StrutsTagAttribute(description="Define required position of required form element (left|right)")
    public void setRequiredPosition(String requiredPosition) {
        this.requiredPosition = requiredPosition;
    }

    @StrutsTagAttribute(description="Define error position of form element (top|bottom)")
    public void setErrorPosition(String errorPosition) {
        this.errorPosition = errorPosition;
    }

    @StrutsTagAttribute(description="The name to set for element")
    public void setName(String name) {
        this.name = name;
    }

    @StrutsTagAttribute(description="If set to true, the rendered element will indicate that input is required", type="Boolean", defaultValue="false")
    public void setRequiredLabel(String requiredLabel) {
        this.requiredLabel = requiredLabel;
    }

    @StrutsTagAttribute(description="Set the html tabindex attribute on rendered html element")
    public void setTabindex(String tabindex) {
        this.tabindex = tabindex;
    }

    @StrutsTagAttribute(description="Preset the value of input element.")
    public void setValue(String value) {
        this.value = value;
    }

    @StrutsTagAttribute(description="Set the html onclick attribute on rendered html element")
    public void setOnclick(String onclick) {
        this.onclick = onclick;
    }

    @StrutsTagAttribute(description="Set the html ondblclick attribute on rendered html element")
    public void setOndblclick(String ondblclick) {
        this.ondblclick = ondblclick;
    }

    @StrutsTagAttribute(description="Set the html onmousedown attribute on rendered html element")
    public void setOnmousedown(String onmousedown) {
        this.onmousedown = onmousedown;
    }

    @StrutsTagAttribute(description="Set the html onmouseup attribute on rendered html element")
    public void setOnmouseup(String onmouseup) {
        this.onmouseup = onmouseup;
    }

    @StrutsTagAttribute(description="Set the html onmouseover attribute on rendered html element")
    public void setOnmouseover(String onmouseover) {
        this.onmouseover = onmouseover;
    }

    @StrutsTagAttribute(description="Set the html onmousemove attribute on rendered html element")
    public void setOnmousemove(String onmousemove) {
        this.onmousemove = onmousemove;
    }

    @StrutsTagAttribute(description="Set the html onmouseout attribute on rendered html element")
    public void setOnmouseout(String onmouseout) {
        this.onmouseout = onmouseout;
    }

    @StrutsTagAttribute(description="Set the html onfocus attribute on rendered html element")
    public void setOnfocus(String onfocus) {
        this.onfocus = onfocus;
    }

    @StrutsTagAttribute(description=" Set the html onblur attribute on rendered html element")
    public void setOnblur(String onblur) {
        this.onblur = onblur;
    }

    @StrutsTagAttribute(description="Set the html onkeypress attribute on rendered html element")
    public void setOnkeypress(String onkeypress) {
        this.onkeypress = onkeypress;
    }

    @StrutsTagAttribute(description="Set the html onkeydown attribute on rendered html element")
    public void setOnkeydown(String onkeydown) {
        this.onkeydown = onkeydown;
    }

    @StrutsTagAttribute(description="Set the html onkeyup attribute on rendered html element")
    public void setOnkeyup(String onkeyup) {
        this.onkeyup = onkeyup;
    }

    @StrutsTagAttribute(description="Set the html onselect attribute on rendered html element")
    public void setOnselect(String onselect) {
        this.onselect = onselect;
    }

    @StrutsTagAttribute(description="Set the html onchange attribute on rendered html element")
    public void setOnchange(String onchange) {
        this.onchange = onchange;
    }

    @StrutsTagAttribute(description="Set the html accesskey attribute on rendered html element")
    public void setAccesskey(String accesskey) {
        this.accesskey = accesskey;
    }

    @StrutsTagAttribute(description="Set the tooltip of this particular component")
    public void setTooltip(String tooltip) {
        this.tooltip = tooltip;
    }

    @StrutsTagAttribute(description="Deprecated. Use individual tooltip configuration attributes instead.")
    public void setTooltipConfig(String tooltipConfig) {
        this.tooltipConfig = tooltipConfig;
    }

    @StrutsTagAttribute(description="Set the key (name, value, label) for this particular component")
    public void setKey(String key) {
        this.key = key;
    }

    @StrutsTagAttribute(description="Use JavaScript to generate tooltips", type="Boolean", defaultValue="false")
    public void setJavascriptTooltip(String javascriptTooltip) {
        this.javascriptTooltip = javascriptTooltip;
    }

    @StrutsTagAttribute(description="CSS class applied to JavaScrip tooltips", defaultValue="StrutsTTClassic")
    public void setTooltipCssClass(String tooltipCssClass) {
        this.tooltipCssClass = tooltipCssClass;
    }

    @StrutsTagAttribute(description="Delay in milliseconds, before showing JavaScript tooltips ",
        defaultValue="Classic")
    public void setTooltipDelay(String tooltipDelay) {
        this.tooltipDelay = tooltipDelay;
    }

    @StrutsTagAttribute(description="Icon path used for image that will have the tooltip")
    public void setTooltipIconPath(String tooltipIconPath) {
        this.tooltipIconPath = tooltipIconPath;
    }

    public void setDynamicAttributes(Map<String, String> tagDynamicAttributes) {
        for (Map.Entry<String, String> entry : tagDynamicAttributes.entrySet()) {
            String attrName = entry.getKey();
            String attrValue = entry.getValue();

            if (!isValidTagAttribute(attrName)) {
                if (ComponentUtils.containsExpression(attrValue) && !lazyEvaluation()) {
                    String translated = TextParseUtil.translateVariables('%', attrValue, stack);
                    dynamicAttributes.put(attrName, ObjectUtils.defaultIfNull(translated, attrValue));
                } else {
                    dynamicAttributes.put(attrName, attrValue);
                }
            }
        }
    }

    /**
     * supports dynamic attributes for freemarker ui tags
     * @see <a href="https://issues.apache.org/jira/browse/WW-3174">WW-3174</a>
     * @see <a href="https://issues.apache.org/jira/browse/WW-4166">WW-4166</a>
     */
    @Override
    public void copyParams(Map<String, Object> params) {
        super.copyParams(params);
        for (Map.Entry<String, Object>entry : params.entrySet()) {
            String entryKey = entry.getKey();
            if (!isValidTagAttribute(entryKey) && !entryKey.equals("dynamicAttributes")) {
                dynamicAttributes.put(entryKey, entry.getValue());
            }
        }
    }

    /**
     * Used to avoid evaluating attributes in {@link #evaluateParams()} or {@link #evaluateExtraParams()}
     * as evaluation will happen in tag's template
     *
     * @return boolean false if evaluation should be performed in ftl
     */
    protected boolean lazyEvaluation() {
        return false;
    }

}
