DefaultValidatorFileParser.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.validator;

import org.apache.struts2.ActionContext;
import org.apache.struts2.ObjectFactory;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.config.providers.XmlHelper;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.DomHelper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Parse the validation file. (eg. MyAction-validation.xml, MyAction-actionAlias-validation.xml)
 * to return a List of ValidatorConfig encapsulating the validator information.
 *
 * @author Jason Carreira
 * @author James House
 * @author tm_jee ( tm_jee (at) yahoo.co.uk )
 * @author Rob Harrop
 * @author Rene Gielen
 * @author Martin Gilday
 *
 * @see ValidatorConfig
 */
public class DefaultValidatorFileParser implements ValidatorFileParser {

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

    static final String DEFAULT_MULTI_TEXTVALUE_SEPARATOR = " ";
    static final String MULTI_TEXTVALUE_SEPARATOR_CONFIG_KEY = "xwork.validatorfileparser.multi_textvalue_separator";

    private ObjectFactory objectFactory;
    private String multiTextvalueSeparator=DEFAULT_MULTI_TEXTVALUE_SEPARATOR;

    @Inject(value=MULTI_TEXTVALUE_SEPARATOR_CONFIG_KEY, required = false)
    public void setMultiTextvalueSeparator(String type) {
        multiTextvalueSeparator = type;
    }

    public String getMultiTextvalueSeparator() {
        return multiTextvalueSeparator;
    }

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

    public List<ValidatorConfig> parseActionValidatorConfigs(ValidatorFactory validatorFactory, InputStream is, final String resourceName) {
        List<ValidatorConfig> validatorCfgs = new ArrayList<>();

        InputSource in = new InputSource(is);
        in.setSystemId(resourceName);

        Map<String, String> dtdMappings = new HashMap<>();
        dtdMappings.put("-//Apache Struts//XWork Validator 1.0//EN", "xwork-validator-1.0.dtd");
        dtdMappings.put("-//Apache Struts//XWork Validator 1.0.2//EN", "xwork-validator-1.0.2.dtd");
        dtdMappings.put("-//Apache Struts//XWork Validator 1.0.3//EN", "xwork-validator-1.0.3.dtd");
        dtdMappings.put("-//Apache Struts//XWork Validator Config 1.0//EN", "xwork-validator-config-1.0.dtd");

        Document doc = DomHelper.parse(in, dtdMappings);

        if (doc != null) {
            NodeList fieldNodes = doc.getElementsByTagName("field");

            // BUG: xw-305: Let validator be parsed first and hence added to
            // the beginning of list and therefore evaluated first, so short-circuting
            // it will not cause field-level validator to be kicked off.
            {
                NodeList validatorNodes = doc.getElementsByTagName("validator");
                addValidatorConfigs(validatorFactory, validatorNodes, new HashMap<>(), validatorCfgs);
            }

            for (int i = 0; i < fieldNodes.getLength(); i++) {
                Element fieldElement = (Element) fieldNodes.item(i);
                String fieldName = fieldElement.getAttribute("name");
                Map<String, Object> extraParams = new HashMap<>();
                extraParams.put("fieldName", fieldName);

                NodeList validatorNodes = fieldElement.getElementsByTagName("field-validator");
                addValidatorConfigs(validatorFactory, validatorNodes, extraParams, validatorCfgs);
            }
        }

        return validatorCfgs;
    }


    public void parseValidatorDefinitions(Map<String, String> validators, InputStream is, String resourceName) {

        InputSource in = new InputSource(is);
        in.setSystemId(resourceName);

        Map<String, String> dtdMappings = new HashMap<>();
        dtdMappings.put("-//Apache Struts//XWork Validator Config 1.0//EN", "xwork-validator-config-1.0.dtd");
        dtdMappings.put("-//Apache Struts//XWork Validator Definition 1.0//EN", "xwork-validator-definition-1.0.dtd");

        Document doc = DomHelper.parse(in, dtdMappings);

        if (doc != null) {
            NodeList nodes = doc.getElementsByTagName("validator");

            for (int i = 0; i < nodes.getLength(); i++) {
                Element validatorElement = (Element) nodes.item(i);
                String name = validatorElement.getAttribute("name");
                String className = validatorElement.getAttribute("class");

                try {
                    // catch any problems here
                    objectFactory.buildValidator(className, new HashMap<>(), ActionContext.getContext().getContextMap());
                    validators.put(name, className);
                } catch (Exception e) {
                    throw new ConfigurationException("Unable to load validator class " + className, e, validatorElement);
                }
            }
        }
    }

    /**
     * Extract trimmed text value from the given DOM element, ignoring XML comments. Appends all CharacterData nodes
     * and EntityReference nodes into a single String value, excluding Comment nodes.
     * This method is based on a method originally found in DomUtils class of Springframework.
     *
     * @param valueEle the given DOM element
     * @return the text value
     *
     * @see org.w3c.dom.CharacterData
     * @see org.w3c.dom.EntityReference
     * @see org.w3c.dom.Comment
     */
    public String getTextValue(Element valueEle) {
        StringBuilder value = new StringBuilder();
        NodeList nl = valueEle.getChildNodes();
        boolean firstCDataFound = false;
        for (int i = 0; i < nl.getLength(); i++) {
            Node item = nl.item(i);
            if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) {
                final String nodeValue = item.getNodeValue();
                if (nodeValue != null) {
                    if (firstCDataFound) {
                        value.append(getMultiTextvalueSeparator());
                    } else {
                        firstCDataFound = true;
                    }
                    value.append(nodeValue.trim());
                }
            }
        }
        return value.toString().trim();
    }

    private void addValidatorConfigs(ValidatorFactory factory, NodeList validatorNodes, Map<String, Object> extraParams, List<ValidatorConfig> validatorCfgs) {
        for (int j = 0; j < validatorNodes.getLength(); j++) {
            Element validatorElement = (Element) validatorNodes.item(j);
            String validatorType = validatorElement.getAttribute("type");
            Map<String, Object> params = new HashMap<>(extraParams);

            params.putAll(XmlHelper.getParams(validatorElement));

            // ensure that the type is valid...
            try {
                factory.lookupRegisteredValidatorType(validatorType);
            } catch (IllegalArgumentException ex) {
                throw new ConfigurationException("Invalid validation type: " + validatorType, validatorElement);
            }

            ValidatorConfig.Builder vCfg = new ValidatorConfig.Builder(validatorType)
                    .addParams(params)
                    .location(DomHelper.getLocationObject(validatorElement))
                    .shortCircuit(Boolean.parseBoolean(validatorElement.getAttribute("short-circuit")));

            NodeList messageNodes = validatorElement.getElementsByTagName("message");
            Element messageElement = (Element) messageNodes.item(0);

            final Node defaultMessageNode = messageElement.getFirstChild();
            String defaultMessage = (defaultMessageNode == null) ? "" : defaultMessageNode.getNodeValue();
            vCfg.defaultMessage(defaultMessage);

            Map<String, String> messageParams = XmlHelper.getParams(messageElement);
            String key = messageElement.getAttribute("key");


            if (!key.trim().isEmpty()) {
                vCfg.messageKey(key);

                // Get the default message when pattern 2 is used. We are only interested in the
                // i18n message parameters when an i18n message key is specified.
                // pattern 1:
                // <message key="someKey">Default message</message>
                // pattern 2:
                // <message key="someKey">
                //     <param name="1">'param1'</param>
                //     <param name="2">'param2'</param>
                //     <param name="defaultMessage>The Default Message</param>
                // </message>

                if (messageParams.containsKey("defaultMessage")) {
                    vCfg.defaultMessage(messageParams.get("defaultMessage"));
                }

                // Sort the message param. those with keys as '1', '2', '3' etc. (numeric values)
                // are i18n message parameter, others are excluded.
                TreeMap<Integer, String> sortedMessageParameters = new TreeMap<>();
                for (Map.Entry<String, String> messageParamEntry : messageParams.entrySet()) {

                    try {
                        int _order = Integer.parseInt(messageParamEntry.getKey());
                        sortedMessageParameters.put(_order, messageParamEntry.getValue());
                    }
                    catch (NumberFormatException e) {
                        // ignore if its not numeric.
                    }
                }
                vCfg.messageParams(sortedMessageParameters.values().toArray(new String[0]));
            } else {
                if (!messageParams.isEmpty()) {
                    // we are i18n message parameters defined but no i18n message,
                    // let's warn the user.
                    if (LOG.isWarnEnabled()) {
                        LOG.warn("validator of type [{}] have i18n message parameters defined but no i18n message key, it's parameters will be ignored", validatorType);
                    }
                }
            }

            validatorCfgs.add(vCfg.build());
        }
    }
}