Form.java

/*
 * 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 org.apache.struts2.ObjectFactory;
import org.apache.struts2.config.Configuration;
import org.apache.struts2.config.RuntimeConfiguration;
import org.apache.struts2.config.entities.ActionConfig;
import org.apache.struts2.config.entities.InterceptorMapping;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.MethodFilterInterceptorUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.validator.ActionValidatorManager;
import org.apache.struts2.validator.FieldValidator;
import org.apache.struts2.validator.ValidationException;
import org.apache.struts2.validator.ValidationInterceptor;
import org.apache.struts2.validator.Validator;
import org.apache.struts2.validator.ValidatorContext;
import org.apache.struts2.validator.validators.VisitorFieldValidator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * <!-- START SNIPPET: javadoc -->
 * <p>
 * Renders HTML an input form.
 * </p>
 *
 * <p>
 * The remote form allows the form to be submitted without the page being refreshed. The results from the form
 * can be inserted into any HTML element on the page.
 * </p>
 * <p>
 * NOTE:<br>
 * The order / logic in determining the posting url of the generated HTML form is as follows:
 * </p>
 *
 * <ol>
 * <li>
 * If the action attribute is not specified, then the current request will be used to
 * determine the posting url
 * </li>
 * <li>
 * If the action is given, Struts will try to obtain an ActionConfig. This will be
 * successful if the action attribute is a valid action alias defined struts.xml.
 * </li>
 * <li>
 * If the action is given and is not an action alias defined in struts.xml, Struts
 * will used the action attribute as if it is the posting url, separting the namespace
 * from it and using UrlHelper to generate the final url.
 * </li>
 * </ol>
 * <p>
 * <!-- END SNIPPET: javadoc -->
 *
 * <p><b>Examples</b></p>
 *
 * <pre>
 * <!-- START SNIPPET: example -->
 *
 * &lt;s:form ... /&gt;
 *
 * <!-- END SNIPPET: example -->
 * </pre>
 */
@StrutsTag(
    name = "form",
    tldTagClass = "org.apache.struts2.views.jsp.ui.FormTag",
    description = "Renders an input form",
    allowDynamicAttributes = true)
public class Form extends ClosingUIBean {
    public static final String OPEN_TEMPLATE = "form";
    public static final String TEMPLATE = "form-close";

    private int sequence = 0;

    protected String onsubmit;
    protected String onreset;
    protected String action;
    protected String target;
    protected String enctype;
    protected String method;
    protected String namespace;
    protected String validate;
    protected String portletMode;
    protected String windowState;
    protected String acceptcharset;
    protected boolean includeContext = true;

    protected String focusElement;
    protected Configuration configuration;
    protected ObjectFactory objectFactory;
    protected UrlRenderer urlRenderer;
    protected ActionValidatorManager actionValidatorManager;

    public Form(ValueStack stack, HttpServletRequest request, HttpServletResponse response) {
        super(stack, request, response);
    }

    @Override
    protected boolean evaluateNameValue() {
        return false;
    }

    @Override
    public String getDefaultOpenTemplate() {
        return OPEN_TEMPLATE;
    }

    @Override
    protected String getDefaultTemplate() {
        return TEMPLATE;
    }

    @Inject
    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    @Inject
    public void setObjectFactory(ObjectFactory objectFactory) {
        this.objectFactory = objectFactory;
    }

    @Inject
    public void setUrlRenderer(UrlRenderer urlRenderer) {
        this.urlRenderer = urlRenderer;
    }

    @Inject
    public void setActionValidatorManager(ActionValidatorManager mgr) {
        this.actionValidatorManager = mgr;
    }


    /*
     * Revised for Portlet actionURL as form action, and add wwAction as hidden
     * field. Refer to template.simple/form.vm
     */
    @Override
    protected void evaluateExtraParams() {
        super.evaluateExtraParams();
        if (validate != null) {
            addParameter("validate", findValue(validate, Boolean.class));
        }

        if (name == null) {
            //make the name the same as the id
            String id = (String) getAttributes().get("id");
            if (StringUtils.isNotEmpty(id)) {
                addParameter("name", id);
            }
        }

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

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

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

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

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

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

        // keep a collection of the tag names for anything special the templates might want to do (such as pure client
        // side validation)
        if (!attributes.containsKey("tagNames")) {
            // we have this if check so we don't do this twice (on open and close of the template)
            addParameter("tagNames", new ArrayList());
        }

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

    /**
     * Form component determine the its HTML element id as follows:-
     * <ol>
     *    <li>if an 'id' attribute is specified.</li>
     *    <li>if an 'action' attribute is specified, it will be used as the id.</li>
     * </ol>
     */
    @Override
    protected void populateComponentHtmlId(Form form) {
        if (id != null) {
            super.populateComponentHtmlId(null);
        }

        // if no id given, it will be tried to generate it from the action attribute
        // by the urlRenderer implementation
        urlRenderer.renderFormUrl(this);
    }

    /**
     * Evaluate client side JavaScript Enablement.
     *
     * @param actionName   the actioName to check for
     * @param namespace    the namespace to check for
     * @param actionMethod the method to ckeck for
     */
    protected void evaluateClientSideJsEnablement(String actionName, String namespace, String actionMethod) {

        // Only evaluate if Client-Side js is to be enable when validate=true
        Boolean validate = (Boolean) getAttributes().get("validate");
        if (validate != null && validate) {

            addParameter("performValidation", Boolean.FALSE);

            RuntimeConfiguration runtimeConfiguration = configuration.getRuntimeConfiguration();
            ActionConfig actionConfig = runtimeConfiguration.getActionConfig(namespace, actionName);

            if (actionConfig != null) {
                List<InterceptorMapping> interceptors = actionConfig.getInterceptors();
                for (InterceptorMapping interceptorMapping : interceptors) {
                    if (interceptorMapping.getInterceptor() instanceof ValidationInterceptor validationInterceptor) {

                        Set<String> excludeMethods = validationInterceptor.getExcludeMethodsSet();
                        Set<String> includeMethods = validationInterceptor.getIncludeMethodsSet();

                        if (MethodFilterInterceptorUtil.applyMethod(excludeMethods, includeMethods, actionMethod)) {
                            addParameter("performValidation", Boolean.TRUE);
                        }
                        return;
                    }
                }
            }
        }
    }

    public List getValidators(String name) {
        Class actionClass = (Class) getAttributes().get("actionClass");
        if (actionClass == null) {
            return Collections.EMPTY_LIST;
        }

        String formActionValue = findString(action);
        ActionMapping mapping = actionMapper.getMappingFromActionName(formActionValue);

        if (mapping == null) {
            mapping = actionMapper.getMappingFromActionName((String) getAttributes().get("actionName"));
        }

        if (mapping == null) {
            return Collections.EMPTY_LIST;
        }

        String actionName = mapping.getName();

        String methodName = null;
        if (isValidateAnnotatedMethodOnly(actionName)) {
            methodName = mapping.getMethod();
        }

        List<Validator> actionValidators = actionValidatorManager.getValidators(actionClass, actionName, methodName);
        List<Validator> validators = new ArrayList<>();

        findFieldValidators(name, actionClass, actionName, actionValidators, validators, "");

        return validators;
    }

    private boolean isValidateAnnotatedMethodOnly(String actionName) {
        RuntimeConfiguration runtimeConfiguration = configuration.getRuntimeConfiguration();
        String actionNamespace = getNamespace(stack);
        ActionConfig actionConfig = runtimeConfiguration.getActionConfig(actionNamespace, actionName);

        if (actionConfig != null) {
            List<InterceptorMapping> interceptors = actionConfig.getInterceptors();
            for (InterceptorMapping interceptorMapping : interceptors) {
                if (interceptorMapping.getInterceptor() instanceof ValidationInterceptor validationInterceptor) {
                    return validationInterceptor.isValidateAnnotatedMethodOnly();
                }
            }
        }
        return false;
    }

    private void findFieldValidators(String name, Class actionClass, String actionName,
                                     List<Validator> validatorList, List<Validator> resultValidators, String prefix) {

        for (Validator validator : validatorList) {
            if (validator instanceof FieldValidator fieldValidator) {

                if (validator instanceof VisitorFieldValidator) {
                    VisitorFieldValidator vfValidator = (VisitorFieldValidator) fieldValidator;
                    Class clazz = getVisitorReturnType(actionClass, vfValidator.getFieldName());
                    if (clazz == null) {
                        continue;
                    }

                    List<Validator> visitorValidators = actionValidatorManager.getValidators(clazz, actionName);
                    String vPrefix = prefix + (vfValidator.isAppendPrefix() ? vfValidator.getFieldName() + "." : "");
                    findFieldValidators(name, clazz, actionName, visitorValidators, resultValidators, vPrefix);
                } else if ((prefix + fieldValidator.getFieldName()).equals(name)) {
                    if (StringUtils.isNotBlank(prefix)) {
                        //fixing field name for js side
                        FieldVisitorValidatorWrapper wrap = new FieldVisitorValidatorWrapper(fieldValidator, prefix);
                        resultValidators.add(wrap);
                    } else {
                        resultValidators.add(fieldValidator);
                    }
                }
            }
        }
    }

    /**
     * Wrap field validator, add visitor's field prefix to the field name.
     * Javascript side is not aware of the visitor validators
     * and does not know how to prefix the fields.
     */
    /*
     * Class is public because Freemarker has problems accessing properties.
     */
    public static class FieldVisitorValidatorWrapper implements FieldValidator {
        private FieldValidator fieldValidator;
        private String namePrefix;

        public FieldVisitorValidatorWrapper(FieldValidator fv, String namePrefix) {
            this.fieldValidator = fv;
            this.namePrefix = namePrefix;
        }

        public String getValidatorType() {
            return "field-visitor";
        }

        public String getFieldName() {
            return namePrefix + fieldValidator.getFieldName();
        }

        public FieldValidator getFieldValidator() {
            return fieldValidator;
        }

        public void setFieldValidator(FieldValidator fieldValidator) {
            this.fieldValidator = fieldValidator;
        }

        public String getDefaultMessage() {
            return fieldValidator.getDefaultMessage();
        }

        public String getMessage(Object object) {
            return fieldValidator.getMessage(object);
        }

        public String getMessageKey() {
            return fieldValidator.getMessageKey();
        }

        public String[] getMessageParameters() {
            return fieldValidator.getMessageParameters();
        }

        public ValidatorContext getValidatorContext() {
            return fieldValidator.getValidatorContext();
        }

        public void setDefaultMessage(String message) {
            fieldValidator.setDefaultMessage(message);
        }

        public void setFieldName(String fieldName) {
            fieldValidator.setFieldName(fieldName);
        }

        public void setMessageKey(String key) {
            fieldValidator.setMessageKey(key);
        }

        public void setMessageParameters(String[] messageParameters) {
            fieldValidator.setMessageParameters(messageParameters);
        }

        public void setValidatorContext(ValidatorContext validatorContext) {
            fieldValidator.setValidatorContext(validatorContext);
        }

        public void setValidatorType(String type) {
            fieldValidator.setValidatorType(type);
        }

        public void setValueStack(ValueStack stack) {
            fieldValidator.setValueStack(stack);
        }

        public void validate(Object object) throws ValidationException {
            fieldValidator.validate(object);
        }

        public String getNamePrefix() {
            return namePrefix;
        }

        public void setNamePrefix(String namePrefix) {
            this.namePrefix = namePrefix;
        }
    }

    /**
     * Return type of visited object.
     *
     * @param actionClass      action class
     * @param visitorFieldName field name
     * @return type of visited object
     */
    @SuppressWarnings("unchecked")
    protected Class getVisitorReturnType(Class actionClass, String visitorFieldName) {
        if (visitorFieldName == null) {
            return null;
        }
        String methodName = "get" + StringUtils.capitalize(visitorFieldName);
        try {
            Method method = actionClass.getMethod(methodName);
            return method.getReturnType();
        } catch (NoSuchMethodException e) {
            return null;
        }
    }


    /**
     * Get a incrementing sequence unique to this <code>Form</code> component.
     * It is used by <code>Form</code> component's child that might need a
     * sequence to make them unique.
     *
     * @return int
     */
    protected int getSequence() {
        return sequence++;
    }

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

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

    @StrutsTagAttribute(description = "Set action name to submit to, without .action suffix", defaultValue = "current action")
    public void setAction(String action) {
        this.action = action;
    }

    @StrutsTagAttribute(description = "HTML form target attribute")
    public void setTarget(String target) {
        this.target = target;
    }

    @StrutsTagAttribute(description = "HTML form enctype attribute")
    public void setEnctype(String enctype) {
        this.enctype = enctype;
    }

    @StrutsTagAttribute(description = "HTML form method attribute")
    public void setMethod(String method) {
        this.method = method;
    }

    @StrutsTagAttribute(description = "Namespace for action to submit to", defaultValue = "current namespace")
    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    @StrutsTagAttribute(description = "Whether client side/remote validation should be performed. Only" +
        " useful with theme xhtml/ajax", type = "Boolean", defaultValue = "false")
    public void setValidate(String validate) {
        this.validate = validate;
    }

    @StrutsTagAttribute(description = "The portlet mode to display after the form submit")
    public void setPortletMode(String portletMode) {
        this.portletMode = portletMode;
    }

    @StrutsTagAttribute(description = "The window state to display after the form submit")
    public void setWindowState(String windowState) {
        this.windowState = windowState;
    }

    @StrutsTagAttribute(description = "The accepted charsets for this form. The values may be comma or blank delimited.")
    public void setAcceptcharset(String acceptcharset) {
        this.acceptcharset = acceptcharset;
    }

    @StrutsTagAttribute(description = "Id of element that will receive the focus when page loads.")
    public void setFocusElement(String focusElement) {
        this.focusElement = focusElement;
    }

    @StrutsTagAttribute(description = "Whether actual context should be included in URL", type = "Boolean", defaultValue = "true")
    public void setIncludeContext(boolean includeContext) {
        this.includeContext = includeContext;
    }
}