com.vaadin.ui.declarative.DesignContext.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.ui.declarative.DesignContext.java

Source

/*
 * Copyright 2000-2018 Vaadin Ltd.
 *
 * Licensed 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 com.vaadin.ui.declarative;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;

import com.vaadin.annotations.DesignRoot;
import com.vaadin.server.Constants;
import com.vaadin.server.DeploymentConfiguration;
import com.vaadin.server.VaadinService;
import com.vaadin.shared.Registration;
import com.vaadin.ui.Component;
import com.vaadin.ui.HasComponents;
import com.vaadin.ui.declarative.Design.ComponentFactory;
import com.vaadin.ui.declarative.Design.ComponentMapper;

/**
 * This class contains contextual information that is collected when a component
 * tree is constructed based on HTML design template. This information includes
 * mappings from local ids, global ids and captions to components , as well as a
 * mapping between prefixes and package names (such as "vaadin" ->
 * "com.vaadin.ui").
 *
 * Versions prior to 7.6 use "v" as the default prefix. Versions starting with
 * 7.6 support reading designs with either "v" or "vaadin" as the prefix, but
 * only write "vaadin" by default. Writing with the legacy prefix can be
 * activated with the property or context parameter
 * {@link Constants#SERVLET_PARAMETER_LEGACY_DESIGN_PREFIX}.
 *
 * @since 7.4
 * @author Vaadin Ltd
 */
public class DesignContext implements Serializable {

    private static final String LEGACY_PREFIX = "v";
    private static final String VAADIN_PREFIX = "vaadin";
    private static final String VAADIN7_PREFIX = "vaadin7";

    private static final String VAADIN_UI_PACKAGE = "com.vaadin.ui";
    private static final String VAADIN7_UI_PACKAGE = "com.vaadin.v7.ui";

    // cache for object instances
    private static Map<Class<?>, Component> instanceCache = new ConcurrentHashMap<>();

    // The root component of the component hierarchy
    private Component rootComponent = null;
    // Attribute names for global id and caption and the prefix name for a local
    // id
    public static final String ID_ATTRIBUTE = "id";
    public static final String CAPTION_ATTRIBUTE = "caption";
    public static final String LOCAL_ID_ATTRIBUTE = "_id";
    // Mappings from ids to components. Modified when reading from design.
    private Map<String, Component> idToComponent = new HashMap<>();
    private Map<String, Component> localIdToComponent = new HashMap<>();
    private Map<String, Component> captionToComponent = new HashMap<>();
    // Mapping from components to local ids. Accessed when writing to
    // design. Modified when reading from design.
    private Map<Component, String> componentToLocalId = new HashMap<>();
    private Document doc; // required for calling createElement(String)
    // namespace mappings
    private Map<String, String> packageToPrefix = new HashMap<>();
    private Map<String, String> prefixToPackage = new HashMap<>();
    private final Map<Component, Map<String, String>> customAttributes = new HashMap<>();

    // component creation listeners
    private List<ComponentCreationListener> listeners = new ArrayList<>();

    private ShouldWriteDataDelegate shouldWriteDataDelegate = ShouldWriteDataDelegate.DEFAULT;

    // this cannot be static because of testability issues
    private Boolean legacyDesignPrefix = null;

    private boolean shouldWriteDefaultValues = false;

    public DesignContext(Document doc) {
        this.doc = doc;
        // Initialize the mapping between prefixes and package names.
        if (isLegacyPrefixEnabled()) {
            addPackagePrefix(LEGACY_PREFIX, VAADIN_UI_PACKAGE);
            prefixToPackage.put(VAADIN_PREFIX, VAADIN_UI_PACKAGE);
        } else {
            addPackagePrefix(VAADIN_PREFIX, VAADIN_UI_PACKAGE);
            prefixToPackage.put(LEGACY_PREFIX, VAADIN_UI_PACKAGE);
        }
        addPackagePrefix(VAADIN7_PREFIX, VAADIN7_UI_PACKAGE);
    }

    public DesignContext() {
        this(new Document(""));
    }

    /**
     * Returns a component having the specified local id. If no component is
     * found, returns null.
     *
     * @param localId
     *            The local id of the component
     * @return a component whose local id equals localId
     */
    public Component getComponentByLocalId(String localId) {
        return localIdToComponent.get(localId);
    }

    /**
     * Returns a component having the specified global id. If no component is
     * found, returns null.
     *
     * @param globalId
     *            The global id of the component
     * @return a component whose global id equals globalId
     */
    public Component getComponentById(String globalId) {
        return idToComponent.get(globalId);
    }

    /**
     * Returns a component having the specified caption. If no component is
     * found, returns null.
     *
     * @param caption
     *            The caption of the component
     * @return a component whose caption equals the caption given as a parameter
     */
    public Component getComponentByCaption(String caption) {
        return captionToComponent.get(caption);
    }

    /**
     * Creates a mapping between the given global id and the component. Returns
     * true if globalId was already mapped to some component. Otherwise returns
     * false. Also sets the id of the component to globalId.
     *
     * If there is a mapping from the component to a global id (gid) different
     * from globalId, the mapping from gid to component is removed.
     *
     * If the string was mapped to a component c different from the given
     * component, the mapping from c to the string is removed. Similarly, if
     * component was mapped to some string s different from globalId, the
     * mapping from s to component is removed.
     *
     * @param globalId
     *            The new global id of the component.
     * @param component
     *            The component whose global id is to be set.
     * @return true, if there already was a global id mapping from the string to
     *         some component.
     */
    private boolean mapId(String globalId, Component component) {
        Component oldComponent = idToComponent.get(globalId);
        if (oldComponent != null && !oldComponent.equals(component)) {
            oldComponent.setId(null);
        }
        String oldGID = component.getId();
        if (oldGID != null && !oldGID.equals(globalId)) {
            idToComponent.remove(oldGID);
        }
        component.setId(globalId);
        idToComponent.put(globalId, component);
        return oldComponent != null && !oldComponent.equals(component);
    }

    /**
     * Creates a mapping between the given local id and the component. Returns
     * true if localId was already mapped to some component or if component was
     * mapped to some string. Otherwise returns false.
     *
     * If the string was mapped to a component c different from the given
     * component, the mapping from c to the string is removed. Similarly, if
     * component was mapped to some string s different from localId, the mapping
     * from s to component is removed.
     *
     * @since 7.5.0
     *
     * @param component
     *            The component whose local id is to be set.
     * @param localId
     *            The new local id of the component.
     *
     * @return true, if there already was a local id mapping from the string to
     *         some component or from the component to some string. Otherwise
     *         returns false.
     */
    public boolean setComponentLocalId(Component component, String localId) {
        return twoWayMap(localId, component, localIdToComponent, componentToLocalId);
    }

    /**
     * Returns the local id for a component.
     *
     * @since 7.5.0
     *
     * @param component
     *            The component whose local id to get.
     * @return the local id of the component, or null if the component has no
     *         local id assigned
     */
    public String getComponentLocalId(Component component) {
        return componentToLocalId.get(component);
    }

    /**
     * Creates a mapping between the given caption and the component. Returns
     * true if caption was already mapped to some component.
     *
     * Note that unlike mapGlobalId, if some component already has the given
     * caption, the caption is not cleared from the component. This allows
     * non-unique captions. However, only one of the components corresponding to
     * a given caption can be found using the map captionToComponent. Hence, any
     * captions that are used to identify an object should be unique.
     *
     * @param caption
     *            The new caption of the component.
     * @param component
     *            The component whose caption is to be set.
     * @return true, if there already was a caption mapping from the string to
     *         some component.
     */
    private boolean mapCaption(String caption, Component component) {
        return captionToComponent.put(caption, component) != null;
    }

    /**
     * Creates a two-way mapping between key and value, i.e. adds key -> value
     * to keyToValue and value -> key to valueToKey. If key was mapped to a
     * value v different from the given value, the mapping from v to key is
     * removed. Similarly, if value was mapped to some key k different from key,
     * the mapping from k to value is removed.
     *
     * Returns true if there already was a mapping from key to some value v or
     * if there was a mapping from value to some key k. Otherwise returns false.
     *
     * @param key
     *            The new key in keyToValue.
     * @param value
     *            The new value in keyToValue.
     * @param keyToValue
     *            A map from keys to values.
     * @param valueToKey
     *            A map from values to keys.
     * @return whether there already was some mapping from key to a value or
     *         from value to a key.
     */
    private <S, T> boolean twoWayMap(S key, T value, Map<S, T> keyToValue, Map<T, S> valueToKey) {
        T oldValue = keyToValue.put(key, value);
        if (oldValue != null && !oldValue.equals(value)) {
            valueToKey.remove(oldValue);
        }
        S oldKey = valueToKey.put(value, key);
        if (oldKey != null && !oldKey.equals(key)) {
            keyToValue.remove(oldKey);
        }
        return oldValue != null || oldKey != null;
    }

    /**
     * Creates a two-way mapping between a prefix and a package name.
     *
     * Note that modifying the mapping for {@value #VAADIN_UI_PACKAGE} may
     * invalidate the backwards compatibility mechanism supporting reading such
     * components with either {@value #LEGACY_PREFIX} or {@value #VAADIN_PREFIX}
     * as prefix.
     *
     * @param prefix
     *            the prefix name without an ending dash (for instance, "vaadin"
     *            is by default used for "com.vaadin.ui")
     * @param packageName
     *            the name of the package corresponding to prefix
     *
     * @see #getPackagePrefixes()
     * @see #getPackagePrefix(String)
     * @see #getPackage(String)
     * @since 7.5.0
     */
    public void addPackagePrefix(String prefix, String packageName) {
        twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix);
    }

    /**
     * Gets the prefix mapping for a given package, or <code>null</code> if
     * there is no mapping for the package.
     *
     * @see #addPackagePrefix(String, String)
     * @see #getPackagePrefixes()
     *
     * @since 7.5.0
     * @param packageName
     *            the package name to get a prefix for
     * @return the prefix for the package, or <code>null</code> if no prefix is
     *         registered
     */
    public String getPackagePrefix(String packageName) {
        if (VAADIN_UI_PACKAGE.equals(packageName)) {
            return isLegacyPrefixEnabled() ? LEGACY_PREFIX : VAADIN_PREFIX;
        } else {
            return packageToPrefix.get(packageName);
        }
    }

    /**
     * Gets all registered package prefixes.
     *
     *
     * @since 7.5.0
     * @see #getPackage(String)
     * @return a collection of package prefixes
     */
    public Collection<String> getPackagePrefixes() {
        return Collections.unmodifiableCollection(prefixToPackage.keySet());
    }

    /**
     * Gets the package corresponding to the give prefix, or <code>null</code>
     * no package has been registered for the prefix.
     *
     * @since 7.5.0
     * @see #addPackagePrefix(String, String)
     * @param prefix
     *            the prefix to find a package for
     * @return the package prefix, or <code>null</code> if no package is
     *         registered for the provided prefix
     */
    public String getPackage(String prefix) {
        return prefixToPackage.get(prefix);
    }

    /**
     * Returns the default instance for the given class. The instance must not
     * be modified by the caller.
     *
     * @param component
     * @return the default instance for the given class. The return value must
     *         not be modified by the caller
     */
    public <T> T getDefaultInstance(Component component) {
        // If the root is a @DesignRoot component, it can't use itself as a
        // reference or the written design will be empty

        // If the root component in some other way initializes itself in the
        // constructor
        if (getRootComponent() == component && component.getClass().isAnnotationPresent(DesignRoot.class)) {
            return (T) getDefaultInstance((Class<? extends Component>) component.getClass().getSuperclass());
        }
        return (T) getDefaultInstance(component.getClass());
    }

    private Component getDefaultInstance(Class<? extends Component> componentClass) {
        Component instance = instanceCache.get(componentClass);
        if (instance == null) {
            instance = instantiateClass(componentClass.getName());
            instanceCache.put(componentClass, instance);
        }
        return instance;
    }

    /**
     * Reads and stores the mappings from prefixes to package names from meta
     * tags located under <head> in the html document.
     */
    protected void readPackageMappings(Document doc) {
        Element head = doc.head();
        if (head == null) {
            return;
        }
        for (Node child : head.childNodes()) {
            if (child instanceof Element) {
                Element childElement = (Element) child;
                if ("meta".equals(childElement.tagName())) {
                    Attributes attributes = childElement.attributes();
                    if (attributes.hasKey("name") && attributes.hasKey("content")
                            && "package-mapping".equals(attributes.get("name"))) {
                        String contentString = attributes.get("content");
                        String[] parts = contentString.split(":");
                        if (parts.length != 2) {
                            throw new DesignException("The meta tag '" + child + "' cannot be parsed.");
                        }
                        String prefixName = parts[0];
                        String packageName = parts[1];
                        addPackagePrefix(prefixName, packageName);
                    }
                }
            }
        }
    }

    /**
     * Writes the package mappings (prefix -> package name) of this object to
     * the specified document.
     * <p>
     * The prefixes are stored as <meta> tags under <head> in the document.
     *
     * @param doc
     *            the Jsoup document tree where the package mappings are written
     */
    public void writePackageMappings(Document doc) {
        Element head = doc.head();
        for (String prefix : getPackagePrefixes()) {
            // Only store the prefix-name mapping if it is not a default mapping
            // (such as "vaadin" -> "com.vaadin.ui")
            if (!VAADIN_PREFIX.equals(prefix) && !VAADIN7_PREFIX.equals(prefix) && !LEGACY_PREFIX.equals(prefix)) {
                Node newNode = doc.createElement("meta");
                newNode.attr("name", "package-mapping");
                String prefixToPackageName = prefix + ":" + getPackage(prefix);
                newNode.attr("content", prefixToPackageName);
                head.appendChild(newNode);
            }
        }
    }

    /**
     * Check whether the legacy prefix "v" or the default prefix "vaadin" should
     * be used when writing designs. The property or context parameter
     * {@link Constants#SERVLET_PARAMETER_LEGACY_DESIGN_PREFIX} can be used to
     * switch to the legacy prefix.
     *
     * @since 7.5.7
     * @return true to use the legacy prefix, false by default
     */
    protected boolean isLegacyPrefixEnabled() {
        if (legacyDesignPrefix != null) {
            return legacyDesignPrefix.booleanValue();
        }
        if (VaadinService.getCurrent() == null) {
            // This will happen at least in JUnit tests.
            return false;
        }
        DeploymentConfiguration configuration = VaadinService.getCurrent().getDeploymentConfiguration();
        legacyDesignPrefix = configuration
                .getApplicationOrSystemProperty(Constants.SERVLET_PARAMETER_LEGACY_DESIGN_PREFIX, "false")
                .equals("true");
        return legacyDesignPrefix.booleanValue();
    }

    /**
     * Creates an html tree node corresponding to the given element. Also
     * initializes its attributes by calling writeDesign. As a result of the
     * writeDesign() call, this method creates the entire subtree rooted at the
     * returned Node.
     *
     * @param childComponent
     *            The component with state that is written in to the node
     * @return An html tree node corresponding to the given component. The tag
     *         name of the created node is derived from the class name of
     *         childComponent.
     */
    public Element createElement(Component childComponent) {
        ComponentMapper componentMapper = Design.getComponentMapper();

        String tagName = componentMapper.componentToTag(childComponent, this);

        Element newElement = doc.createElement(tagName);
        childComponent.writeDesign(newElement, this);
        // Handle the local id. Global id and caption should have been taken
        // care of by writeDesign.
        String localId = componentToLocalId.get(childComponent);
        if (localId != null) {
            newElement.attr(LOCAL_ID_ATTRIBUTE, localId);
        }
        return newElement;
    }

    /**
     * Reads the given design node and creates the corresponding component tree.
     *
     * @param componentDesign
     *            The design element containing the description of the component
     *            to be created.
     * @return the root component of component tree
     */
    public Component readDesign(Element componentDesign) {
        // Create the component.
        Component component = instantiateComponent(componentDesign);
        readDesign(componentDesign, component);
        fireComponentCreatedEvent(componentToLocalId.get(component), component);
        return component;
    }

    /**
     *
     * Reads the given design node and populates the given component with the
     * corresponding component tree.
     * <p>
     * Additionally registers the component id, local id and caption of the
     * given component and all its children in the context
     *
     * @param componentDesign
     *            The design element containing the description of the component
     *            to be created
     * @param component
     *            The component which corresponds to the design element
     */
    public void readDesign(Element componentDesign, Component component) {
        component.readDesign(componentDesign, this);
        // Get the ids and the caption of the component and store them in the
        // maps of this design context.
        org.jsoup.nodes.Attributes attributes = componentDesign.attributes();
        // global id: only update the mapping, the id has already been set for
        // the component
        String id = component.getId();
        if (id != null && !id.isEmpty()) {
            boolean mappingExists = mapId(id, component);
            if (mappingExists) {
                throw new DesignException("The following global id is not unique: " + id);
            }
        }
        // local id: this is not a property of a component, so need to fetch it
        // from the attributes of componentDesign
        if (attributes.hasKey(LOCAL_ID_ATTRIBUTE)) {
            String localId = attributes.get(LOCAL_ID_ATTRIBUTE);
            boolean mappingExists = setComponentLocalId(component, localId);
            if (mappingExists) {
                throw new DesignException("the following local id is not unique: " + localId);
            }
        }
        // caption: a property of a component, possibly not unique
        String caption = component.getCaption();
        if (caption != null) {
            mapCaption(caption, component);
        }
    }

    /**
     * Creates a Component corresponding to the given node. Does not set the
     * attributes for the created object.
     *
     * @param node
     *            a node of an html tree
     * @return a Component corresponding to node, with no attributes set.
     */
    private Component instantiateComponent(Node node) {
        String tag = node.nodeName();

        ComponentMapper componentMapper = Design.getComponentMapper();
        Component component = componentMapper.tagToComponent(tag, Design.getComponentFactory(), this);

        assert tagEquals(tag, componentMapper.componentToTag(component, this));

        return component;
    }

    private boolean tagEquals(String tag1, String tag2) {
        return tag1.equals(tag2) || (hasVaadinPrefix(tag1) && hasVaadinPrefix(tag2));
    }

    private boolean hasVaadinPrefix(String tag) {
        return tag.startsWith(LEGACY_PREFIX + "-") || tag.startsWith(VAADIN_PREFIX + "-");
    }

    /**
     * Instantiates given class via ComponentFactory.
     *
     * @param qualifiedClassName
     *            class name to instantiate
     * @return instance of a given class
     */
    private Component instantiateClass(String qualifiedClassName) {
        ComponentFactory factory = Design.getComponentFactory();
        Component component = factory.createComponent(qualifiedClassName, this);

        if (component == null) {
            throw new DesignException("Got unexpected null component from " + factory.getClass().getName()
                    + " for class " + qualifiedClassName);
        }

        return component;
    }

    /**
     * Returns the root component of a created component hierarchy.
     *
     * @return the root component of the hierarchy
     */
    public Component getRootComponent() {
        return rootComponent;
    }

    /**
     * Sets the root component of a created component hierarchy.
     *
     * @param rootComponent
     *            the root component of the hierarchy
     */
    public void setRootComponent(Component rootComponent) {
        this.rootComponent = rootComponent;
    }

    /**
     * Adds a component creation listener. The listener will be notified when
     * components are created while parsing a design template
     *
     * @param listener
     *            the component creation listener to be added
     * @return a registration object for removing the listener
     */
    public Registration addComponentCreationListener(ComponentCreationListener listener) {
        listeners.add(listener);
        return () -> listeners.remove(listener);
    }

    /**
     * Removes a component creation listener.
     *
     * @param listener
     *            the component creation listener to be removed
     * @deprecated Use a {@link Registration} object returned by
     *             {@link #addComponentCreationListener(ComponentCreationListener)}
     *             a listener
     */
    @Deprecated
    public void removeComponentCreationListener(ComponentCreationListener listener) {
        listeners.remove(listener);
    }

    /**
     * Fires component creation event
     *
     * @param localId
     *            localId of the component
     * @param component
     *            the component that was created
     */
    private void fireComponentCreatedEvent(String localId, Component component) {
        ComponentCreatedEvent event = new ComponentCreatedEvent(localId, component);
        for (ComponentCreationListener listener : listeners) {
            listener.componentCreated(event);
        }
    }

    /**
     * Interface to be implemented by component creation listeners.
     *
     * @author Vaadin Ltd
     */
    @FunctionalInterface
    public interface ComponentCreationListener extends Serializable {

        /**
         * Called when component has been created in the design context.
         *
         * @param event
         *            the component creation event containing information on the
         *            created component
         */
        public void componentCreated(ComponentCreatedEvent event);
    }

    /**
     * Component creation event that is fired when a component is created in
     * the. context
     *
     * @author Vaadin Ltd
     */
    public class ComponentCreatedEvent implements Serializable {
        private final String localId;
        private final Component component;
        private final DesignContext context;

        /**
         * Creates a new instance of ComponentCreatedEvent
         *
         * @param localId
         *            the local id of the created component
         * @param component
         *            the created component
         */
        private ComponentCreatedEvent(String localId, Component component) {
            this.localId = localId;
            this.component = component;
            context = DesignContext.this;
        }

        /**
         * Returns the local id of the created component or null if not exist.
         *
         * @return the localId
         */
        public String getLocalId() {
            return localId;
        }

        /**
         * Returns the created component.
         *
         * @return the component
         */
        public Component getComponent() {
            return component;
        }

        /**
         * Returns the new component context.
         *
         * @return the context
         *
         * @since 8.5
         */
        public DesignContext getContext() {
            return context;
        }
    }

    /**
     * Helper method for component write implementors to determine whether their
     * children should be written out or not.
     *
     * @param c
     *            The component being written
     * @param defaultC
     *            The default instance for the component
     * @return whether the children of c should be written
     */
    public boolean shouldWriteChildren(Component c, Component defaultC) {
        if (c == getRootComponent()) {
            // The root component should always write its children - otherwise
            // the result is empty
            return true;
        }

        if (defaultC instanceof HasComponents && ((HasComponents) defaultC).iterator().hasNext()) {
            // Easy version which assumes that this is a custom component if the
            // constructor adds children
            return false;
        }

        return true;
    }

    /**
     * Determines whether the container data of a component should be written
     * out by delegating to a {@link ShouldWriteDataDelegate}. The default
     * delegate assumes that all component data is provided by a data provider
     * connected to a back end system and that the data should thus not be
     * written.
     *
     * @since 7.5.0
     * @see #setShouldWriteDataDelegate(ShouldWriteDataDelegate)
     * @param component
     *            the component to check
     * @return <code>true</code> if container data should be written out for the
     *         provided component; otherwise <code>false</code>.
     */
    public boolean shouldWriteData(Component component) {
        return getShouldWriteDataDelegate().shouldWriteData(component);
    }

    /**
     * Sets the delegate that determines whether the container data of a
     * component should be written out.
     *
     * @since 7.5.0
     * @see #shouldWriteChildren(Component, Component)
     * @see #getShouldWriteDataDelegate()
     * @param shouldWriteDataDelegate
     *            the delegate to set, not <code>null</code>
     * @throws IllegalArgumentException
     *             if the provided delegate is <code>null</code>
     */
    public void setShouldWriteDataDelegate(ShouldWriteDataDelegate shouldWriteDataDelegate) {
        if (shouldWriteDataDelegate == null) {
            throw new IllegalArgumentException("Delegate cannot be null");
        }
        this.shouldWriteDataDelegate = shouldWriteDataDelegate;
    }

    /**
     * Gets the delegate that determines whether the container data of a
     * component should be written out.
     *
     * @since 7.5.0
     * @see #setShouldWriteDataDelegate(ShouldWriteDataDelegate)
     * @see #shouldWriteChildren(Component, Component)
     * @return the shouldWriteDataDelegate the currently use delegate
     */
    public ShouldWriteDataDelegate getShouldWriteDataDelegate() {
        return shouldWriteDataDelegate;
    }

    /**
     * Gets the attributes that the component did not handle.
     *
     * @since 7.7
     * @param component
     *            the component to get the attributes for
     * @return map of the attributes which were not recognized by the component
     */
    public Map<String, String> getCustomAttributes(Component component) {
        return customAttributes.get(component);
    }

    /**
     * Sets a custom attribute not handled by the component. These attributes
     * are directly written to the component tag.
     *
     * @since 7.7
     * @param component
     *            the component to set the attribute for
     * @param attribute
     *            the attribute to set
     * @param value
     *            the value of the attribute
     */
    public void setCustomAttribute(Component component, String attribute, String value) {
        Map<String, String> map = customAttributes.get(component);
        if (map == null) {
            customAttributes.put(component, map = new HashMap<>());
        }
        map.put(attribute, value);
    }

    /**
     * Set whether default attribute values should be written by the
     * {@code DesignAttributeHandler#writeAttribute(String, Attributes, Object, Object, Class, DesignContext)}
     * method. Default is {@code false}.
     *
     * @since 8.0
     * @param value
     *            {@code true} to write default values of attributes,
     *            {@code false} to disable writing of default values
     */
    public void setShouldWriteDefaultValues(boolean value) {
        shouldWriteDefaultValues = value;
    }

    /**
     * Determines whether default attribute values should be written by the
     * {@code DesignAttributeHandler#writeAttribute(String, Attributes, Object, Object, Class, DesignContext)}
     * method. Default is {@code false}.
     *
     * @since 8.0
     * @return {@code true} if default values of attributes should be written,
     *         otherwise {@code false}
     */
    public boolean shouldWriteDefaultValues() {
        return shouldWriteDefaultValues;
    }
}