com.vaadin.ui.declarative.DesignContext Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* 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, 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 idToComponent = new HashMap<>();
private Map localIdToComponent = new HashMap<>();
private Map captionToComponent = new HashMap<>();
// Mapping from components to local ids. Accessed when writing to
// design. Modified when reading from design.
private Map componentToLocalId = new HashMap<>();
private Document doc; // required for calling createElement(String)
// namespace mappings
private Map packageToPrefix = new HashMap<>();
private Map prefixToPackage = new HashMap<>();
private final Map> customAttributes = new HashMap<>();
// component creation listeners
private List 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 boolean twoWayMap(S key, T value, Map keyToValue,
Map 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 null
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 null
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 getPackagePrefixes() {
return Collections.unmodifiableCollection(prefixToPackage.keySet());
}
/**
* Gets the package corresponding to the give prefix, or null
* 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 null
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
* a component class
* @param component
* the component that determines the class
* @return the default instance for the given class. The return value must
* not be modified by the caller
*/
@SuppressWarnings("unchecked")
public 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.
*
* @param doc
* the 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.
*
* 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.
*
* 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 true
if container data should be written out for the
* provided component; otherwise false
.
*/
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 null
* @throws IllegalArgumentException
* if the provided delegate is null
*/
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 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 map = customAttributes.get(component);
if (map == null) {
map = new HashMap<>();
customAttributes.put(component, map);
}
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;
}
}