All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.granite.ui.components.AttrBuilder Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2012 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.granite.ui.components;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;

import com.adobe.granite.xss.XSSAPI;

/**
 * A builder to generate HTML attributes. This builder is designed to be secured
 * using {@link XSSAPI}. It will encode the value automatically. If the value is
 * {@code null}, it will be ignored.
 */
public class AttrBuilder {
    private final HttpServletRequest req;

    private final XSSAPI xssAPI;

    @Nonnull
    private Map data = new LinkedHashMap();

    private Map encodings = new LinkedHashMap<>();

    private Set classes = new HashSet();

    private enum Encoding {
        HREF, HTML_ATTR
    }

    public AttrBuilder(@Nonnull HttpServletRequest req, @Nonnull XSSAPI xssAPI) {
        this.req = req;
        this.xssAPI = xssAPI;
    }

    /**
     * Gets the raw {@link Map} of attributes, with un-encoded values
     *
     * @return {@link Map} of attributes
     */
    @Nonnull
    public Map getData() {
        return data;
    }

    /**
     * Adds relationship. Currently it is implemented as {@code class} attribute.
     *
     * @param value
     *            the relationship to add
     */
    public void addRel(@CheckForNull String value) {
        addClass(value);
    }

    /**
     * Adds {@code class} attribute with the given value.
     *
     * @param value
     *            the class attribute to add
     */
    public void addClass(@CheckForNull String value) {
        if (StringUtils.isBlank(value)) {
            return;
        }

        if (classes.add(value)) {
            add("class", value);
        }
    }

    /**
     * Adds an attribute that behave like {@code href} attribute. i.e. the value
     * will be prepended with context path (if absolute path) and checked using
     * {@link XSSAPI#getValidHref(String)}.
     *
     * @param name
     *            the name of the attribute to add
     * @param value
     *            the value of the specified attribute
     */
    public void addHref(@CheckForNull String name, @CheckForNull String value) {
        if (value == null || StringUtils.isBlank(value) || StringUtils.isBlank(name)) {
            return;
        }

        if (value.startsWith("/")) {
            value = req.getContextPath() + value;
        }

        data.put(name, value);
        encodings.put(name, Encoding.HREF);
    }

    /**
     * Adds {@code disabled} attribute.
     *
     * @param disabled
     *            the boolean value of the {@code disabled} attribute
     */
    public void addDisabled(boolean disabled) {
        this.addBoolean("disabled", disabled);
    }

    /**
     * Adds {@code checked} attribute.
     *
     * @param checked
     *            the boolean value of the {@code checked} attribute
     */
    public void addChecked(boolean checked) {
        this.addBoolean("checked", checked);
    }

    /**
     * Adds {@code selected} attribute.
     *
     * @param selected
     *            the boolean value of the {@code selected} attribute
     */
    public void addSelected(boolean selected) {
        this.addBoolean("selected", selected);
    }

    /**
     * Adds {@code multiple} attribute.
     *
     * @param multiple
     *            the boolean value of the {@code multiple} attribute
     */
    public void addMultiple(boolean multiple) {
        this.addBoolean("multiple", multiple);
    }

    /**
     * Adds boolean attribute (behaves like {@code disabled}) for the given name.
     *
     * When the given value is {@code true}, it will be printed as "disabled=''",
     * instead of "disabled='true'". When the given value is {@code false}, it will
     * NOT be printed, instead of "disabled='false'".
     *
     * @param name
     *            the name of the boolean attribute to add
     * @param value
     *            the boolean value of the attribute
     */
    public void addBoolean(@CheckForNull String name, boolean value) {
        if (!value) {
            return;
        }
        add(name, "");
    }

    /**
     * Adds the given name as {@code data-*} attribute.
     *
     * @param name
     *            the name of the {@code data-*} attribute to add
     * @param value
     *            the value of the attribute
     */
    public void addOther(@CheckForNull String name, @CheckForNull String value) {
        if (StringUtils.isBlank(name)) {
            return;
        }
        add("data-" + xssAPI.encodeForHTML(name), value);
    }

    /**
     * Adds the given data as {@code data-*} attributes. Entries with keys specified
     * in exclusions parameter or having namespace (e.g. "jcr:primaryType") will be
     * excluded.
     *
     * @param data
     *            the map containing key/value pairs to add as {@code data-*}
     *            attributes
     * @param exclusions
     *            the keys which must not be added as {@code data-*} attributes
     */
    public void addOthers(@Nonnull Map data, @Nonnull String... exclusions) {
        List blacklisted = Arrays.asList(exclusions);

        for (Entry e : data.entrySet()) {
            String key = e.getKey();

            if (key.indexOf(":") >= 0) {
                continue;
            }
            if (blacklisted.indexOf(key) >= 0) {
                continue;
            }

            Object value = e.getValue();

            if (value.getClass().isArray()) {
                for (Object o : (Object[]) value) {
                    addOther(key, o.toString());
                }
            } else {
                addOther(key, value.toString());
            }
        }
    }

    /**
     * Adds attribute with the given name. The value will be added to existing
     * attribute using space-delimited convention. e.g. class="class1 class2"
     *
     * @param name
     *            the name of the attribute to add
     * @param value
     *            the boolean value of the attribute
     */
    @SuppressWarnings("null")
    public void add(@CheckForNull String name, @CheckForNull Boolean value) {
        if (value == null || name == null || StringUtils.isBlank(name)) {
            return;
        }

        addNoCheck(name, value.toString());
    }

    /**
     * Adds attribute with the given name. The value will be added to existing
     * attribute using space-delimited convention. e.g. class="class1 class2"
     *
     * @param name
     *            the name of the attribute to add
     * @param value
     *            the integer value of the attribute
     */
    @SuppressWarnings("null")
    public void add(@CheckForNull String name, @CheckForNull Integer value) {
        if (value == null || name == null || StringUtils.isBlank(name)) {
            return;
        }

        addNoCheck(name, value.toString());
    }

    /**
     * Adds attribute with the given name. The value will be added to existing
     * attribute using space-delimited convention. e.g. class="class1 class2"
     *
     * @param name
     *            the name of the attribute to add
     * @param value
     *            the double value of the attribute
     */
    @SuppressWarnings("null")
    public void add(@CheckForNull String name, @CheckForNull Double value) {
        if (value == null || name == null || StringUtils.isBlank(name)) {
            return;
        }

        addNoCheck(name, value.toString());
    }

    /**
     * Adds attribute with the given name. The value will be added to existing
     * attribute using space-delimited convention. e.g. class="class1 class2"
     *
     * @param name
     *            the name of the attribute to add
     * @param value
     *            the string value of the attribute
     */
    public void add(@CheckForNull String name, @CheckForNull String value) {
        if (value == null || name == null || StringUtils.isBlank(name)) {
            return;
        }
        addNoCheck(name, value);
        encodings.put(name, Encoding.HTML_ATTR);
    }

    /**
     * Sets attribute with the given name. Existing value previously set will be
     * replaced by the given value.
     *
     * @param name
     *            the name of the attribute to set or replace (if exists)
     * @param value
     *            the string value of the attribute
     */
    public void set(@CheckForNull String name, @CheckForNull String value) {
        if (value == null || name == null || StringUtils.isBlank(name)) {
            return;
        }
        data.put(name, value);
        encodings.put(name, Encoding.HTML_ATTR);
    }

    private void addNoCheck(@Nonnull String name, @Nonnull String v) {
        if (data.containsKey(name)) {
            v = data.get(name) + " " + v;
        }

        data.put(name, v);
    }

    /**
     * Returns {@code true} if there is no attribute in this builder, {@code false}
     * otherwise.
     *
     * @return {@code true} if there is no attribute in this builder, {@code false}
     *         otherwise
     */
    public boolean isEmpty() {
        return data.isEmpty();
    }

    /**
     * Builds the attributes in the form of {@code =''*}.
     *
     * @return the string containing the built attributes
     */
    public String build() {
        try {
            StringWriter out = new StringWriter();
            build(out);
            return out.toString();
        } catch (IOException impossible) {
            throw new RuntimeException(impossible);
        }
    }

    /**
     * Builds the attributes in the form of {@code =''*}*.
     *
     * @param out
     *            the writer
     * @throws IOException
     *             in case there's an error when appending to the writer
     */
    public void build(@Nonnull Writer out) throws IOException {
        for (Entry e : data.entrySet()) {
            String key = e.getKey();
            String value = e.getValue();

            Encoding encoding = encodings.get(key);

            if (encoding != null && value.length() > 0) {
                switch (encoding) {
                    case HREF:
                        value = xssAPI.getValidHref(value);
                        break;
                    default:
                        value = xssAPI.encodeForHTMLAttr(value);
                        break;
                }
            }

            out.append(" ").append(key).append("=\"").append(value).append("\"");
        }
    }

    @Override
    public String toString() {
        return build();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy