Text.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.util.ValueStack;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.util.TextProviderHelper;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * <!-- START SNIPPET: javadoc -->
 * <p>
 * Render a I18n text message.
 * </p>
 *
 * <p>
 * The message must be in a resource bundle
 * with the same name as the action that it is associated with. In practice
 * this means that you should create a properties file in the same package
 * as your Java class with the same name as your class, but with .properties
 * extension.
 * </p>
 *
 * <p>
 * If the named message is not found in a property file, then the body of the
 * tag will be used as default message. If no value is found, the key of the
 * message will not be written out.
 * </p>
 *
 * <p>
 * Note: If the <b>var</b> attribute is used with this tag, the tag's value will
 * <em>not</em> be written out.  Instead the result will be saved into the
 * action context (action scope).
 * </p>
 * <!-- END SNIPPET: javadoc -->
 *
 * <!-- START SNIPPET: params -->
 *
 * <ul>
 *      <li>name* (String) - the i18n message key</li>
 *      <li>escapeHtml (Boolean) - Escape HTML. Defaults to false</li>
 *      <li>escapeJavaScript (Boolean) - Escape JavaScript. Defaults to false</li>
 *      <li>escapeXml (Boolean) - Escape XML. Defaults to false</li>
 *      <li>escapeCsv (Boolean) - Escape CSV. Defaults to false</li>
 * </ul>
 *
 * <!-- END SNIPPET: params -->
 *
 * <p>
 * Example:
 * </p>
 *
 * <!-- START SNIPPET: exdescription -->
 * <p>Accessing messages from a given bundle (the i18n Shop example bundle in the first example) and using bundle defined through the framework in the second example.</p>
 * <!-- END SNIPPET: exdescription -->
 *
 * <pre>
 * <!-- START SNIPPET: example -->
 *
 * &lt;!-- First Example --&gt;
 * &lt;s:i18n name="struts.action.test.i18n.Shop"&gt;
 *     &lt;s:text name="main.title"/&gt;
 * &lt;/s:i18n&gt;
 *
 * &lt;!-- Second Example --&gt;
 * &lt;s:text name="main.title" /&gt;
 *
 * &lt;!-- Third Examlpe --&gt;
 * &lt;s:text name="i18n.label.greetings"&gt;
 *    &lt;s:param &gt;Mr Smith&lt;/s:param&gt;
 * &lt;/s:text&gt;
 *
 * <!-- END SNIPPET: example -->
 * </pre>
 *
 *
 * <pre>
 * <!-- START SNIPPET: i18nExample -->
 *
 * &lt;-- Fourth Example --&gt;
 * &lt;s:text name="some.key" /&gt;
 *
 * &lt;-- Fifth Example --&gt;
 * &lt;s:text name="some.invalid.key" &gt;
 *    The Default Message That Will Be Displayed
 * &lt;/s:text&gt;
 *
 * <!-- END SNIPPET: i18nExample -->
 * </pre>
 *
 * @see Param
 *
 */
@StrutsTag(
    name="text",
    tldTagClass="org.apache.struts2.views.jsp.TextTag",
    description="Render a I18n text message")
public class Text extends ContextBean implements Param.UnnamedParametric {

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

    protected List<Object> values = Collections.emptyList();
    protected String actualName;
    protected String name;
    private boolean escapeHtml = false;
    private boolean escapeJavaScript = false;
    private boolean escapeXml = false;
    private boolean escapeCsv = false;

    public Text(ValueStack stack) {
        super(stack);
    }

    @StrutsTagAttribute(description = "Name of resource property to fetch", required = true)
    public void setName(String name) {
        this.name = name;
    }

    @StrutsTagAttribute(description="Whether to escape HTML", type="Boolean", defaultValue="false")
    public void setEscapeHtml(boolean escape) {
        this.escapeHtml = escape;
    }

    @StrutsTagAttribute(description="Whether to escape Javascript", type="Boolean", defaultValue="false")
    public void setEscapeJavaScript(boolean escapeJavaScript) {
        this.escapeJavaScript = escapeJavaScript;
    }

    @StrutsTagAttribute(description="Whether to escape XML", type="Boolean", defaultValue="false")
    public void setEscapeXml(boolean escapeXml) {
        this.escapeXml = escapeXml;
    }

    @StrutsTagAttribute(description="Whether to escape CSV (useful to escape a value for a column)", type="Boolean", defaultValue="false")
    public void setEscapeCsv(boolean escapeCsv) {
        this.escapeCsv = escapeCsv;
    }

    @Override
    public boolean usesBody() {
        // overriding this to true such that EVAL_BODY_BUFFERED is return and
        // bodyContent will be valid hence, text between start & end tag will
        // be honoured as default message (WW-1268)
        return true;
    }

    @Override
    public boolean end(Writer writer, String body) {
        actualName = findString(name, "name", "You must specify the i18n key. Example: welcome.header");
        String defaultMessage;
        if (StringUtils.isNotEmpty(body)) {
            defaultMessage = body;
        } else {
            defaultMessage = actualName;
        }

        String msg = TextProviderHelper.getText(actualName, defaultMessage, values, getStack());

        if (msg != null) {
            try {
                if (getVar() == null) {
                    writer.write(prepare(msg));
                } else {
                    putInContext(msg);
                }
            } catch (IOException e) {
                LOG.error("Could not write out Text tag", e);
            }
        }

        return super.end(writer, "");
    }

    @Override
    public void addParameter(String key, Object value) {
        addParameter(value);
    }

    @Override
    public void addParameter(Object value) {
        if (values.isEmpty()) {
            values = new ArrayList<>(4);
        }

        values.add(value);
    }

    private String prepare(String value) {
        String result = value;
        if (escapeHtml) {
            result = StringEscapeUtils.escapeHtml4(result);
        }
        if (escapeJavaScript) {
            result = StringEscapeUtils.escapeEcmaScript(result);
        }
        if (escapeXml) {
            result = StringEscapeUtils.escapeXml10(result);
        }
        if (escapeCsv) {
            result = StringEscapeUtils.escapeCsv(result);
        }

        return result;
    }
}