Skip to content

Commit

Permalink
feat: WebComponent more Jackson (#21101)
Browse files Browse the repository at this point in the history
* feat: WebComponent more Jackson

Change more parts of webComponent
to use Jackson instead of elemental.

* Fix review identified issues
  • Loading branch information
caalador authored Mar 11, 2025
1 parent 442b051 commit e852ba5
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;

import com.vaadin.flow.component.webcomponent.PropertyConfiguration;
import com.vaadin.flow.component.webcomponent.WebComponent;
import com.vaadin.flow.component.webcomponent.WebComponentConfiguration;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.JacksonUtils;
import com.vaadin.flow.internal.ReflectTools;
import com.vaadin.flow.server.webcomponent.PropertyConfigurationImpl;
import com.vaadin.flow.server.webcomponent.PropertyData;
Expand Down Expand Up @@ -90,8 +93,9 @@ public abstract class WebComponentExporter<C extends Component>
implements Serializable {

private static final List<Class> SUPPORTED_TYPES = Collections
.unmodifiableList(Arrays.asList(Boolean.class, String.class,
Integer.class, Double.class, JsonValue.class));
.unmodifiableList(
Arrays.asList(Boolean.class, String.class, Integer.class,
Double.class, JsonValue.class, JsonNode.class));

private final String tag;
private HashMap<String, PropertyConfigurationImpl<C, ? extends Serializable>> propertyConfigurationMap = new HashMap<>();
Expand Down Expand Up @@ -245,11 +249,31 @@ public final PropertyConfiguration<C, Boolean> addProperty(String name,
* default value of property.
* @return fluent {@code PropertyConfiguration} for configuring the property
*/
@Deprecated
public final PropertyConfiguration<C, JsonValue> addProperty(String name,
JsonValue defaultValue) {
return addProperty(name, JsonValue.class, defaultValue);
}

/**
* Add an {@code JsonValue} property to the exported web component
* identified by {@code name}.
*
* @param name
* name of the property. While all formats are allowed, names in
* camelCase will be converted to dash-separated form, when
* property update events are generated, using form
* "property-name-changed", if the property is called
* "propertyName"
* @param defaultValue
* default value of property.
* @return fluent {@code PropertyConfiguration} for configuring the property
*/
public final PropertyConfiguration<C, BaseJsonNode> addProperty(String name,
BaseJsonNode defaultValue) {
return addProperty(name, BaseJsonNode.class, defaultValue);
}

/**
* If custom initialization for the created {@link Component} instance is
* needed, it can be done here. It is also possible to configure custom
Expand Down Expand Up @@ -343,6 +367,14 @@ public Set<PropertyData<? extends Serializable>> getPropertyDataSet() {
public WebComponentBinding<C> createWebComponentBinding(
Instantiator instantiator, Element element,
JsonObject newAttributeDefaults) {
return createWebComponentBinding(instantiator, element,
JacksonUtils.mapElemental(newAttributeDefaults));
}

@Override
public WebComponentBinding<C> createWebComponentBinding(
Instantiator instantiator, Element element,
JsonNode newAttributeDefaults) {
assert (instantiator != null);

final C componentReference = instantiator
Expand Down Expand Up @@ -374,8 +406,8 @@ public WebComponentBinding<C> createWebComponentBinding(
componentReference);

// collect possible new defaults from attributes as JsonValues
final Map<String, JsonValue> newDefaultValues = Stream
.of(newAttributeDefaults.keys()).collect(Collectors
final Map<String, JsonNode> newDefaultValues = JacksonUtils
.getKeys(newAttributeDefaults).stream().collect(Collectors
.toMap(key -> key, newAttributeDefaults::get));

// bind properties onto the WebComponentBinding. Since
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.io.Serializable;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.WebComponentExporter;
import com.vaadin.flow.di.Instantiator;
Expand Down Expand Up @@ -97,6 +99,26 @@ public interface WebComponentConfiguration<C extends Component>
* @return web component binding which can be used by the web component host
* to communicate with the component it is hosting
*/
WebComponentBinding<C> createWebComponentBinding(Instantiator instantiator,
Element element, JsonNode newAttributeDefaults);

/**
* Creates a new {@link WebComponentBinding} instance.
*
* @param instantiator
* {@link com.vaadin.flow.di.Instantiator} used to construct
* instances
* @param element
* element which acts as the root element for the exported
* {@code component} instance
* @param newAttributeDefaults
* {@link JsonObject} containing default overrides set by the
* user defining the component on a web page. These defaults are
* set using the web component's attributes.
* @return web component binding which can be used by the web component host
* to communicate with the component it is hosting
*/
@Deprecated
WebComponentBinding<C> createWebComponentBinding(Instantiator instantiator,
Element element, JsonObject newAttributeDefaults);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonBoolean;
import elemental.json.JsonNull;
import elemental.json.JsonNumber;
import elemental.json.JsonObject;
import elemental.json.JsonValue;

Expand Down Expand Up @@ -153,6 +155,15 @@ public static BaseJsonNode mapElemental(JsonValue jsonValue) {
if (jsonValue == null || jsonValue instanceof JsonNull) {
return nullNode();
}
if (jsonValue instanceof JsonObject) {
return mapElemental((JsonObject) jsonValue);
}
if (jsonValue instanceof JsonNumber) {
return objectMapper.valueToTree(jsonValue.asNumber());
}
if (jsonValue instanceof JsonBoolean) {
return objectMapper.valueToTree(jsonValue.asBoolean());
}
return objectMapper.valueToTree(jsonValue.asString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@
import java.util.HashMap;
import java.util.Objects;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.webcomponent.WebComponentConfiguration;
import com.vaadin.flow.function.SerializableBiConsumer;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.internal.JacksonCodec;
import com.vaadin.flow.internal.JsonCodec;

import elemental.json.Json;
import elemental.json.JsonValue;

/**
Expand All @@ -54,7 +59,7 @@ public final class WebComponentBinding<C extends Component>
* Constructs a new {@code WebComponentBinding}. The bound {@link Component}
* is given via {@code component} parameter. The web component properties
* are bound by calling
* {@link #bindProperty(PropertyConfigurationImpl, boolean, elemental.json.JsonValue)};
* {@link #bindProperty(PropertyConfigurationImpl, boolean, JsonNode)};
*
* @param component
* component which exposes {@code properties} as web component.
Expand Down Expand Up @@ -116,6 +121,7 @@ public void updateProperty(String propertyName, Serializable value) {
* if the {@code jsonValue} cannot be converted to the type of
* the property identified by {@code propertyName}.
*/
@Deprecated
public void updateProperty(String propertyName, JsonValue jsonValue) {
Objects.requireNonNull(propertyName,
"Parameter 'propertyName' must not be null!");
Expand All @@ -127,6 +133,34 @@ public void updateProperty(String propertyName, JsonValue jsonValue) {
updateProperty(propertyName, value);
}

/**
* Updates a property bound to the {@code component}. Converts the {@code
* jsonValue} into the correct type if able and then calls
* {@link #updateProperty(String, java.io.Serializable)}.
*
* @param propertyName
* name of the property, not {@code null}
* @param jsonValue
* new value to set for the property
* @throws NullPointerException
* if {@code propertyName} is {@code null}
* @throws IllegalArgumentException
* if no bound property can be found for {@code propertyName}
* @throws IllegalArgumentException
* if the {@code jsonValue} cannot be converted to the type of
* the property identified by {@code propertyName}.
*/
public void updateProperty(String propertyName, BaseJsonNode jsonValue) {
Objects.requireNonNull(propertyName,
"Parameter 'propertyName' must not be null!");

Class<? extends Serializable> propertyType = getPropertyType(
propertyName);

Serializable value = jsonValueToConcreteType(jsonValue, propertyType);
updateProperty(propertyName, value);
}

/**
* Retrieves the bound {@link Component} instance.
*
Expand Down Expand Up @@ -170,6 +204,69 @@ public void updatePropertiesToComponent() {
properties.forEach((key, value) -> value.notifyValueChange());
}

/**
* Adds a property to {@code this} web component binding based on the {@code
* propertyConfiguration}. If a property with an existing name is bound, the
* previous binding is removed. Starting value for the property is set to
* {@code null}.
*
* @param propertyConfiguration
* property configuration, not {@code null}
* @param overrideDefault
* set to {@code true} if the property should be initialized with
* {@literal null} instead of default value found in
* {@link PropertyData}
* @throws NullPointerException
* if {@code propertyConfiguration} is {@code null}
*/
public void bindProperty(
PropertyConfigurationImpl<C, ? extends Serializable> propertyConfiguration,
boolean overrideDefault) {
bindProperty(propertyConfiguration, overrideDefault, (JsonNode) null);
}

/**
* Adds a property to {@code this} web component binding based on the {@code
* propertyConfiguration}. If a property with an existing name is bound, the
* previous binding is removed.
*
* @param propertyConfiguration
* property configuration, not {@code null}
* @param overrideDefault
* set to {@code true} if the property should be initialized with
* {@code startingValue} instead of default value found in
* {@link PropertyData}
* @param startingValue
* starting value for the property. Can be {@code null}.
* {@code overrideDefault} must be {@code true} for this value to
* have any effect
* @throws NullPointerException
* if {@code propertyConfiguration} is {@code null}
*/
public void bindProperty(
PropertyConfigurationImpl<C, ? extends Serializable> propertyConfiguration,
boolean overrideDefault, JsonNode startingValue) {
Objects.requireNonNull(propertyConfiguration,
"Parameter 'propertyConfiguration' cannot be null!");

final SerializableBiConsumer<C, Serializable> consumer = propertyConfiguration
.getOnChangeHandler();

final Serializable selectedStartingValue = !overrideDefault
? propertyConfiguration.getPropertyData().getDefaultValue()
: jsonValueToConcreteType(startingValue,
propertyConfiguration.getPropertyData().getType());

final PropertyBinding<? extends Serializable> binding = new PropertyBinding<>(
propertyConfiguration.getPropertyData(),
consumer == null ? null
: value -> consumer.accept(component, value),
selectedStartingValue);

properties.put(propertyConfiguration.getPropertyData().getName(),
binding);
}

/**
* Adds a property to {@code this} web component binding based on the {@code
* propertyConfiguration}. If a property with an existing name is bound, the
Expand All @@ -188,6 +285,7 @@ public void updatePropertiesToComponent() {
* @throws NullPointerException
* if {@code propertyConfiguration} is {@code null}
*/
@Deprecated
public void bindProperty(
PropertyConfigurationImpl<C, ? extends Serializable> propertyConfiguration,
boolean overrideDefault, JsonValue startingValue) {
Expand All @@ -212,6 +310,7 @@ public void bindProperty(
binding);
}

@Deprecated
private Serializable jsonValueToConcreteType(JsonValue jsonValue,
Class<? extends Serializable> type) {
Objects.requireNonNull(type, "Parameter 'type' must not be null!");
Expand All @@ -229,6 +328,31 @@ private Serializable jsonValueToConcreteType(JsonValue jsonValue,
}
}

private Serializable jsonValueToConcreteType(JsonNode jsonValue,
Class<? extends Serializable> type) {
Objects.requireNonNull(type, "Parameter 'type' must not be null!");

if (JacksonCodec.canEncodeWithoutTypeInfo(type)) {
Serializable value = null;
if (jsonValue != null) {
value = JacksonCodec.decodeAs(jsonValue, type);
}
return value;
} else if (JsonCodec.canEncodeWithoutTypeInfo(type)) {
// TODO: Remove when ClientCallable works with jackson types only.
Serializable value = null;
if (jsonValue != null && !(jsonValue instanceof NullNode)) {
value = JsonCodec.decodeAs(Json.parse(jsonValue.toString()),
type);
}
return value;
} else {
throw new IllegalArgumentException(
String.format("Received '%s' was not convertible to '%s'",
jsonValue.getClass().getName(), type.getName()));
}
}

private static class PropertyBinding<P extends Serializable>
implements Serializable {
private PropertyData<P> data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import java.util.Objects;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.io.IOUtils;

import com.vaadin.flow.component.Component;
Expand Down Expand Up @@ -253,11 +256,14 @@ private static String getDefaultJsValue(PropertyData<?> property) {
"\\'") + "'";
} else if (JsonValue.class.isAssignableFrom(property.getType())) {
value = ((JsonValue) property.getDefaultValue()).toJson();
} else if (JsonNode.class.isAssignableFrom(property.getType())) {
value = property.getDefaultValue().toString();
} else {
throw new UnsupportedPropertyTypeException(String.format(
"%s is not a currently supported type for a Property."
+ " Please use %s instead.",
+ " Please use %s or %s instead.",
property.getType().getSimpleName(),
JsonNode.class.getSimpleName(),
JsonValue.class.getSimpleName()));
}
if (value == null) {
Expand Down Expand Up @@ -300,9 +306,11 @@ private static String getJSTypeName(PropertyData<?> propertyData) {
return "Number";
} else if (propertyData.getType() == String.class) {
return "String";
} else if (JsonArray.class.isAssignableFrom(propertyData.getType())) {
} else if (JsonArray.class.isAssignableFrom(propertyData.getType())
|| ArrayNode.class.isAssignableFrom(propertyData.getType())) {
return "Array";
} else if (JsonValue.class.isAssignableFrom(propertyData.getType())) {
} else if (JsonValue.class.isAssignableFrom(propertyData.getType())
|| ObjectNode.class.isAssignableFrom(propertyData.getType())) {
return "Object";
} else {
throw new IllegalStateException(
Expand Down
Loading

0 comments on commit e852ba5

Please sign in to comment.