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

com.vaadin.collaborationengine.CollaborationBinder Maven / Gradle / Ivy

/*
 * Copyright 2020-2022 Vaadin Ltd.
 *
 * This program is available under Commercial Vaadin Runtime License 1.0
 * (CVRLv1).
 *
 * For the full License, see http://vaadin.com/license/cvrl-1
 */
package com.vaadin.collaborationengine;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;

import com.vaadin.collaborationengine.HighlightHandler.HighlightContext;
import com.vaadin.collaborationengine.PropertyChangeHandler.PropertyChangeEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.BindingValidationStatusHandler;
import com.vaadin.flow.data.binder.PropertyDefinition;
import com.vaadin.flow.data.binder.Setter;
import com.vaadin.flow.data.converter.Converter;
import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.function.SerializableSupplier;
import com.vaadin.flow.function.ValueProvider;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.shared.Registration;

/**
 * Extension of {@link Binder} for creating collaborative forms with
 * {@link CollaborationEngine}. In addition to Binder's data binding mechanism,
 * CollaborationBinder synchronizes the field values between clients which are
 * connected to the same topic via {@link TopicConnection}.
 *
 * @author Vaadin Ltd
 * @since 1.0
 *
 * @param 
 *            the bean type
 */
public class CollaborationBinder extends Binder
        implements HasExpirationTimeout {

    private static final List> SUPPORTED_CLASS_TYPES = Arrays.asList(
            String.class, Boolean.class, Integer.class, Double.class,
            BigDecimal.class, LocalDate.class, LocalTime.class,
            LocalDateTime.class, Enum.class);
    private static final List> SUPPORTED_COLLECTION_TYPES = Arrays
            .asList(List.class, Set.class);

    static class JsonHandler {
        private final SerializableFunction serializer;
        private final SerializableFunction deserializer;

        private JsonHandler(SerializableFunction serializer,
                SerializableFunction deserializer) {
            this.serializer = serializer;
            this.deserializer = deserializer;
        }

        private static  JsonHandler forBasicType(Class fieldType) {
            return new CollaborationBinder.JsonHandler<>(JsonUtil::toJsonNode,
                    jsonNode -> JsonUtil.toInstance(jsonNode, fieldType));
        }

        private void store(HasValue field) {
            if (field instanceof Component) {
                Component fieldAsComponent = (Component) field;
                ComponentUtil.setData(fieldAsComponent, JsonHandler.class,
                        this);
            } else {
                throw new IllegalArgumentException(
                        "CollaborationBinder can only be used with component fields. The provided field is of type "
                                + field.getClass().getName());
            }
        }

        private static JsonHandler getAndClear(HasValue field) {
            if (field instanceof Component) {
                Component fieldAsComponent = (Component) field;
                JsonHandler config = ComponentUtil.getData(fieldAsComponent,
                        JsonHandler.class);
                ComponentUtil.setData(fieldAsComponent, JsonHandler.class,
                        null);
                return config;
            }

            return null;
        }

        private JsonNode serialize(T value) {
            return serializer.apply(value);
        }

        private T deserialize(JsonNode jsonNode) {
            return deserializer.apply(jsonNode);
        }

    }

    protected static class CollaborationBindingBuilderImpl
            extends BindingBuilderImpl {
        private String propertyName = null;
        private boolean typeIsConverted = false;
        private JsonHandler explicitJsonHandler;

        protected CollaborationBindingBuilderImpl(
                CollaborationBinder binder, HasValue field,
                Converter converterValidatorChain,
                BindingValidationStatusHandler statusHandler) {
            super(binder, field, converterValidatorChain, statusHandler);

            explicitJsonHandler = JsonHandler.getAndClear(field);
        }

        @Override
        @SuppressWarnings({ "rawtypes", "unchecked" })
        public Binding bind(ValueProvider getter,
                Setter setter) {
            // Capture current propertyName
            final String propertyName = this.propertyName;

            if (propertyName == null) {
                throw new UnsupportedOperationException(
                        "A property name must always be provided when binding with the collaboration binder. "
                                + "Use bind(String propertyName) instead.");
            }

            Binding binding = super.bind(getter, setter);

            HasValue field = binding.getField();

            CollaborationBinder binder = getBinder();
            ComponentConnectionContext connectionContext = binder.connectionContext;
            if (connectionContext != null) {
                connectionContext.addComponent((Component) field);
            }

            List registrations = new ArrayList<>();

            registrations.add(field.addValueChangeListener(
                    event -> binder.setMapValueFromField(propertyName, field)));

            registrations.add(FieldHighlighter.setupForField(field,
                    propertyName, binder));

            binder.bindingRegistrations.put(binding,
                    () -> registrations.forEach(Registration::remove));

            binder.fieldToPropertyName.put(field, propertyName);

            return binding;
        }

        @Override
        public Binding bind(String propertyName) {
            try {
                this.propertyName = propertyName;
                return super.bind(propertyName);
            } finally {
                this.propertyName = null;
            }
        }

        @Override
        protected CollaborationBinder getBinder() {
            return (CollaborationBinder) super.getBinder();
        }

        @Override
        protected  BindingBuilder withConverter(
                Converter converter,
                boolean resetNullRepresentation) {
            if (resetNullRepresentation) {
                // Flag implies that this is a "real" converter
                typeIsConverted = true;
            }
            return super.withConverter(converter, resetNullRepresentation);
        }

        @Override
        public BindingBuilder withNullRepresentation(
                TARGET nullRepresentation) {
            /*
             * The null representation is internally implemented as a converter,
             * even though it doesn't change the type of the value. Because of
             * this, we reset the flag to its previous value after it becomes
             * set by the super call that sets a converter.
             */
            boolean typeWasConverted = typeIsConverted;
            try {
                return super.withNullRepresentation(nullRepresentation);
            } finally {
                typeIsConverted = typeWasConverted;
            }
        }

    }

    private final UserInfo localUser;
    private final CollaborationEngine ce;
    private final FieldHighlighter fieldHighlighter;

    private ComponentConnectionContext connectionContext;
    private FormManager formManager;
    private Duration expirationTimeout;

    private final Map, Registration> bindingRegistrations = new HashMap<>();
    private final Map, String> fieldToPropertyName = new HashMap<>();
    private final Map> propertyJsonHandlers = new HashMap<>();
    private final Map, JsonHandler> typeConfigurations = new HashMap<>();

    static {
        UsageStatistics.markAsUsed(
                CollaborationEngine.COLLABORATION_ENGINE_NAME
                        + "/CollaborationBinder",
                CollaborationEngine.COLLABORATION_ENGINE_VERSION);
    }

    /**
     * Creates a new collaboration binder. It uses reflection based on the
     * provided bean type to resolve bean properties.
     * 

* The provided user information is used in the field editing indicators. * The name of the user will be displayed to other users when editing a * field, and the user's color index will be used to set the field's * highlight color. * * @param beanType * the bean type to use, not null * @param localUser * the information of the local user, not null * @since 1.0 */ public CollaborationBinder(Class beanType, UserInfo localUser) { this(beanType, localUser, CollaborationEngine.getInstance()); } CollaborationBinder(Class beanType, UserInfo localUser, CollaborationEngine ce) { super(beanType); this.localUser = Objects.requireNonNull(localUser, "User cannot be null"); this.ce = ce; this.fieldHighlighter = new FieldHighlighter(ce::getUserColorIndex); } @Override protected BindingBuilder configureBinding( BindingBuilder baseBinding, PropertyDefinition definition) { CollaborationBindingBuilderImpl binding = (CollaborationBindingBuilderImpl) baseBinding; JsonHandler handler = findJsonHandler(binding, definition); propertyJsonHandlers.put(definition.getName(), handler); return super.configureBinding(baseBinding, definition); } private static boolean isSupportedType(Type type) { Objects.requireNonNull(type, "Type cannot be null"); if (type instanceof Class) { return isAssignableFromAny(SUPPORTED_CLASS_TYPES, (Class) type); } else if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; if (isAssignableFromAny(SUPPORTED_COLLECTION_TYPES, (Class) parameterizedType.getRawType())) { for (Type typeArgument : parameterizedType .getActualTypeArguments()) { if (!isSupportedType(typeArgument)) { return false; } } return true; } } return false; } private static String createTypeNotSupportedMessage(Type type) { return "The type " + type.getTypeName() + " is not supported. " + "You must use setSerializer to define conversion logic for custom value types. " + "Supported types are: " + Stream.concat(SUPPORTED_CLASS_TYPES.stream(), SUPPORTED_COLLECTION_TYPES.stream()).map(Class::getName) .collect(Collectors.joining(", ")) + ". " + "For collections, the element type must be among the supported types."; } private static boolean isAssignableFromAny(List> types, Class type) { return types.stream() .anyMatch(candidate -> candidate.isAssignableFrom(type)); } private JsonHandler findJsonHandler( CollaborationBindingBuilderImpl builder, PropertyDefinition propertyDefinition) { if (builder.explicitJsonHandler != null) { // Use explicitly defined handler if available return builder.explicitJsonHandler; } if (!builder.typeIsConverted) { Class propertyType = propertyDefinition.getType(); if (isAssignableFromAny(SUPPORTED_COLLECTION_TYPES, propertyType)) { /* * Property is a Collection but it did not have explicit * collection and element types defined */ throw new IllegalStateException( "Cannot configure JSON serializer for '" + builder.propertyName + "' with type '" + propertyType.getName() + "'. For collection " + "types, you have to specify the type of the " + "collection and the type of the elements in " + "the collection. Use " + "CollaborationBinder::forField(field, " + "collectionType, elementType) to specify " + "these. For example, if you are binding a " + "List of String, you should call " + "forField(field, List.class, String.class)."); } /* * Can use the property type as long as there is no converter. A * converter would imply that the bean type is not the same as the * field type. */ return getTypeConfiguration(propertyType) .orElseThrow(() -> new IllegalStateException( "Cannot configure JSON serializer for " + builder.propertyName + ". " + createTypeNotSupportedMessage( propertyType))); } throw new IllegalStateException( "Could not infer field type for property '" + builder.propertyName + "'. Configure the property using an overload of forField or forMemberField that allows explicitly defining the field type."); } private void handlePropertyChange(PropertyChangeEvent event) { getBinding(event.getPropertyName()).map(Binding::getField) .ifPresent(field -> { String propertyName = event.getPropertyName(); JsonNode value = JsonUtil.toJsonNode(event.getValue()); setFieldValueFromFieldState(field, propertyName, value == null ? NullNode.getInstance() : value); }); } private Registration handleHighlight(HighlightContext context) { if (!context.getUser().equals(localUser)) { getBinding(context.getPropertyName()).map(Binding::getField) .ifPresent(field -> fieldHighlighter.addEditor(field, context.getUser(), context.getFieldIndex())); return () -> getBinding(context.getPropertyName()) .map(Binding::getField) .ifPresent(field -> fieldHighlighter.removeEditor(field, context.getUser(), context.getFieldIndex())); } return null; } @SuppressWarnings({ "rawtypes", "unchecked" }) private void setFieldValueFromFieldState(HasValue field, String propertyName, JsonNode stateValue) { if (stateValue instanceof NullNode) { field.clear(); } else { JsonHandler handler = propertyJsonHandlers.get(propertyName); field.setValue(handler.deserialize(stateValue)); } } @SuppressWarnings({ "rawtypes", "unchecked" }) private void setMapValueFromField(String propertyName, HasValue field) { if (formManager != null) { Object value; if (field.isEmpty()) { value = null; } else { JsonHandler handler = propertyJsonHandlers.get(propertyName); value = handler.serialize(field.getValue()); } formManager.setValue(propertyName, value); } } void addEditor(String propertyName, int fieldIndex) { if (formManager != null) { formManager.highlight(propertyName, true, fieldIndex); } } void removeEditor(String propertyName) { if (formManager != null) { formManager.highlight(propertyName, false); } } @Override protected void removeBindingInternal(Binding binding) { // Registration should be removed first, so it can e.g. remove editors // in map. // If the attached component is removed from the context first and the // connection is deactivated, registration removal can't update map // value. Registration registration = bindingRegistrations.remove(binding); if (registration != null) { registration.remove(); } String propertyName = fieldToPropertyName.remove(binding.getField()); propertyJsonHandlers.remove(propertyName); if (connectionContext != null) { connectionContext.removeComponent((Component) binding.getField()); } super.removeBindingInternal(binding); } @Override protected BindingBuilder doCreateBinding( HasValue field, Converter converter, BindingValidationStatusHandler handler) { return new CollaborationBindingBuilderImpl<>(this, field, converter, handler); } /** * Not supported by the collaboration binder! It requires a property name * for binding, so the other overload * {@link CollaborationBinder#bind(HasValue, String)} should be used * instead. *

* See {@link Binder#bind(HasValue, ValueProvider, Setter)} to learn how to * use the method with the regular (non-collaboration) binder. * * @param * the value type of the field * @param field * the field to bind, not null * @param getter * the function to get the value of the property to the field, * not null * @param setter * the function to write the field value to the property or * null if read-only * @return the newly created binding * @throws UnsupportedOperationException * as the method is not supported by the collaboration binder * @deprecated The method does not work with the collaboration binder. Use * {@link CollaborationBinder#bind(HasValue, String)} instead. */ @Override @Deprecated public Binding bind( HasValue field, ValueProvider getter, Setter setter) { return super.bind(field, getter, setter); } /** * Binds the given field to the property with the given name, as described * in {@link Binder#bind(HasValue, String)}. *

* In addition, synchronizes the values with other collaboration binder * instances which are connected to the same topic. * * @param * the value type of the field to bind * @param field * the field to bind, not null * @param propertyName * the name of the property to bind, not null * @return the newly created binding * @throws IllegalArgumentException * if the property name is invalid * @throws IllegalArgumentException * if the property has no accessible getter */ @Override public Binding bind( HasValue field, String propertyName) { return super.bind(field, propertyName); } /** * Binds the member fields found in the given object, as described in * {@link Binder#bindInstanceFields(Object)}. *

* In addition, synchronizes the values with other collaboration binder * instances which are connected to the same topic. * * @param objectWithMemberFields * the object that contains (Java) member fields to bind * @throws IllegalStateException * if there are incompatible HasValue<T> and property * types */ @Override public void bindInstanceFields(Object objectWithMemberFields) { super.bindInstanceFields(objectWithMemberFields); } /** * @deprecated This operation is not supported by the collaboration binder. * You can instead provide the bean for populating the fields * using {@link #setTopic}, and write the values back to the * bean using {@link #writeBean}. */ @Override @Deprecated public void setBean(BEAN bean) { throw new UnsupportedOperationException( "This operation is not supported by the collaboration binder. " + "You can instead provide the bean for populating " + "the fields with the setTopic method, and write the " + "values back to the bean with the writeBean method."); } /** * @deprecated This operation, along with {@link #setBean(Object)}, is not * supported by the collaboration binder. Instead of * {@link #setBean(Object)}, you can provide the bean for * populating the fields using {@link #setTopic}, and write the * values back to the bean using {@link #writeBean}. */ @Override @Deprecated public BEAN getBean() { return super.getBean(); } /** * @deprecated This operation is not supported by the collaboration binder. * You can instead provide the bean for populating the fields * using {@link #setTopic} to avoid overriding currently edited * values. If you explicitly want to reset the field values for * every user currently editing the fields, you can use * {@link #reset}. */ @Override @Deprecated public void readBean(BEAN bean) { throw new UnsupportedOperationException( "This operation is not supported by the collaboration binder. " + "You can instead provide the bean for populating the fields " + "with the setTopic method to avoid overriding currently edited values. " + "If you explicitly want to reset the field values for every user " + "currently editing the fields, you can use the reset method."); } /** * Resets collaborative fields with values from the bound properties of the * given bean. The values will be propagated to all collaborating users. * * @param bean * the bean whose property values to read or {@code null} to * clear bound fields * @since 1.0 */ public void reset(BEAN bean) { super.readBean(bean); } UserInfo getLocalUser() { return localUser; } /** * Sets the topic to use with this binder and initializes the topic contents * if not already set. Setting a topic removes the connection to the * previous topic (if any) and resets all bindings based on values in the * new topic. The bean supplier is used to provide initial values for * bindings in case the topic doesn't yet contain any values. * * * @param topicId * the topic id to use, or null to not use any topic * @param initialBeanSupplier * a supplier that is invoked to get a bean from which to read * initial values. Only invoked if there are no property values * in the topic, or if the topic id is null. * @since 1.0 */ public void setTopic(String topicId, SerializableSupplier initialBeanSupplier) { if (formManager != null) { formManager.close(); formManager = null; fieldToPropertyName.keySet() .forEach(fieldHighlighter::removeEditors); connectionContext = null; } if (topicId == null) { super.readBean(initialBeanSupplier.get()); } else { super.readBean(null); connectionContext = new ComponentConnectionContext(); fieldToPropertyName.keySet().forEach( field -> connectionContext.addComponent((Component) field)); formManager = new FormManager(connectionContext, localUser, topicId, ce); formManager.setActivationHandler(() -> { initializeBindingsWithoutFieldState(initialBeanSupplier); return null; }); formManager.onConnectionFailed( e -> super.readBean(initialBeanSupplier.get())); formManager.setPropertyChangeHandler(this::handlePropertyChange); formManager.setHighlightHandler(this::handleHighlight); if (expirationTimeout != null) { formManager.setExpirationTimeout(expirationTimeout); } } } private void initializeBindingsWithoutFieldState( SerializableSupplier initialBeanSupplier) { List propertiesWithoutFieldState = fieldToPropertyName.values() .stream().filter(propertyName -> formManager .getValue(propertyName, JsonNode.class) == null) .collect(Collectors.toList()); if (propertiesWithoutFieldState.isEmpty()) { return; } BEAN initialBean = initialBeanSupplier.get(); propertiesWithoutFieldState.stream().map(this::getBinding) .filter(Optional::isPresent).map(Optional::get) .forEach(binding -> { if (initialBean == null) { binding.getField().clear(); } else { binding.read(initialBean); } }); } /** * {@inheritDoc} *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. The value type to use for * deserializing the value is automatically determined based on the bean * property type. The type must be defined separately using another overload * of this method in case a converter is used or if the property type is * parameterized. * * @see #forField(HasValue, Class) * @see #forField(HasValue, Class, Class) */ @Override public BindingBuilder forField( HasValue field) { // Overridden only to supplement documentation return super.forField(field); } /** * Creates a new binding for the given field and type. The returned builder * may be further configured before invoking * {@link BindingBuilder#bind(String)} which completes the binding. Until * {@code Binding.bind} is called, the binding has no effect. *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. This method allows * explicitly defining the type to use. This is necessary when a converter * is used since it's then not possible to derive the type from the bean * property. * * @see #forField(HasValue) * @see #forField(HasValue, Class, Class) * @see #setSerializer(Class, SerializableFunction, SerializableFunction) * * @param * the value type of the field * @param field * the field to be bound, not null * @param fieldType * the type of the field value, not null * @return the new binding builder */ public BindingBuilder forField( HasValue field, Class fieldType) { getTypeConfigurationOrThrow(fieldType).store(field); return forField(field); } /** * Creates a new binding for the given (multi select) field whose value type * is a collection. The returned builder may be further configured before * invoking {@link BindingBuilder#bind(String)} which completes the binding. * Until {@code Binding.bind} is called, the binding has no effect. *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. This method allows * explicitly defining the collection type and element type to use. * * @see #forField(HasValue) * @see #forField(HasValue, Class) * @see #setSerializer(Class, SerializableFunction, SerializableFunction) * * @param * the base type of the collection, e.g. {@code Set} for * {@code CheckboxGroup} * @param * the type of the elements in the collection, e.g. * {@code String} for {@code CheckboxGroup} * @param field * the field to be bound, not null * @param collectionType * the base type of the collection, e.g. {@code Set.class} for * {@code CheckboxGroup}, not null * @param elementType * the type of the elements in the collection, e.g. * {@code String.class} for {@code CheckboxGroup}, not * null * @return the new binding builder */ public , ELEMENT> BindingBuilder forField( HasValue field, Class collectionType, Class elementType) { getTypeConfiguration(collectionType, elementType).store(field); return forField(field); } /** * {@inheritDoc} *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. The value type to use for * deserializing the value is automatically determined based on the bean * property type. The type must be defined separately using another overload * of this method in case a converter is used or if the property type is * parameterized. * * @see #forMemberField(HasValue, Class) * @see #forMemberField(HasValue, Class, Class) */ @Override public BindingBuilder forMemberField( HasValue field) { // Overridden only to supplement documentation return super.forMemberField(field); } /** * Creates a new binding for the given field and type. The returned builder * may be further configured before invoking * {@link #bindInstanceFields(Object)}. Unlike with the * {@link #forField(HasValue)} method, no explicit call to * {@link BindingBuilder#bind(String)} is needed to complete this binding in * the case that the name of the field matches a field name found in the * bean. *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. This method allows * explicitly defining the type to use. This is necessary when a converter * is used since it's then not possible to derive the type from the bean * property. * * @see #forMemberField(HasValue) * @see #forMemberField(HasValue, Class, Class) * @see #setSerializer(Class, SerializableFunction, SerializableFunction) * * @param * the value type of the field * @param field * the field to be bound, not null * @param fieldType * @return the new binding builder * * @since 1.0 */ public BindingBuilder forMemberField( HasValue field, Class fieldType) { getTypeConfigurationOrThrow(fieldType).store(field); return forMemberField(field); } /** * Creates a new binding for the given (multi select) field whose value type * is a collection. The returned builder may be further configured before * invoking {@link #bindInstanceFields(Object)}. Unlike with the * * {@link #forField(HasValue)} method, no explicit call to * {@link BindingBuilder#bind(String)} is needed to complete this binding in * the case that the name of the field matches a field name found in the * bean. *

* The field value will be sent over the network to synchronize the value * with other users also editing the same field. This method allows * explicitly defining the collection type and element type to use. * * @see #forMemberField(HasValue) * @see #forMemberField(HasValue, Class) * @see #setSerializer(Class, SerializableFunction, SerializableFunction) * * @param * the base type of the collection, e.g. {@code Set} for * {@code CheckboxGroup} * @param * the type of the elements in the collection, e.g. * {@code String} for {@code CheckboxGroup} * @param field * the field to be bound, not null * @param collectionType * the base type of the collection, e.g. {@code Set.class} for * {@code CheckboxGroup}, not null * @param elementType * the type of the elements in the collection, e.g. * {@code String.class} for {@code CheckboxGroup}, not * null * @return the new binding builder * * @since 1.0 */ public , ELEMENT> BindingBuilder forMemberField( HasValue field, Class collectionType, Class elementType) { getTypeConfiguration(collectionType, elementType).store(field); return forMemberField(field); } /** * Sets a custom serializer and deserializer to use for a specific value * type. The serializer and deserializer will be used for all field bindings * that implicitly or explicitly use that type either as the field type or * as the collection element type in case of multi select fields. It is not * allowed to reconfigure the serializer and deserializer for a previously * configued type nor for any of the basic types that are supported without * custom logic. *

* Field values will be sent over the network to synchronize the value with * other users also editing the same field. This method allows defining * callbacks to convert between the field value and the value that is sent * over the network. This is necessary when using complex objects that are * not suitable to be sent as-is over the network. * * @param * the type handled by the serializer * @param type * the type for which to set a serializer and deserializer, not * null * @param serializer * a callback that receives a non-empty field value and returns * the value to send over the network (not null). * The callback cannot be null. * @param deserializer * a callback that receives a value produced by the serializer * callback (not null) and returns the field value * to use. The callback cannot be null. * @since 1.0 */ public void setSerializer(Class type, SerializableFunction serializer, SerializableFunction deserializer) { Objects.requireNonNull(type, "Type cannot be null"); Objects.requireNonNull(serializer, "Serializer cannot be null"); Objects.requireNonNull(deserializer, "Deserializer cannot be null"); /* * Cannot allow changing an existing serializer on the fly because we * might then not be able to deserialize an existing value */ if (isAssignableFromAny(SUPPORTED_CLASS_TYPES, type) || isAssignableFromAny(SUPPORTED_COLLECTION_TYPES, type)) { throw new IllegalArgumentException( "Cannot set a custom serializer for a type that has built-in support."); } if (typeConfigurations.containsKey(type)) { throw new IllegalStateException( "Serializer has already been set for the type " + type.getName() + "."); } typeConfigurations.put(type, new JsonHandler( value -> new TextNode(serializer.apply(value)), jsonNode -> deserializer.apply(jsonNode.asText()))); } private Optional> getTypeConfiguration( Class fieldType) { @SuppressWarnings("unchecked") JsonHandler configuration = (JsonHandler) typeConfigurations .get(fieldType); if (configuration != null) { return Optional.of(configuration); } else if (isSupportedType(fieldType)) { return Optional.of(JsonHandler.forBasicType(fieldType)); } else { return Optional.empty(); } } private JsonHandler getTypeConfigurationOrThrow( Class fieldType) { return getTypeConfiguration(fieldType) .orElseThrow(() -> new IllegalStateException( createTypeNotSupportedMessage(fieldType))); } private > JsonHandler getTypeConfiguration( Class collectionType, Class elementType) { if (!isAssignableFromAny(SUPPORTED_COLLECTION_TYPES, collectionType)) { throw new IllegalArgumentException(collectionType + " is not supported as a collection. Must use a type assignable to one of " + SUPPORTED_COLLECTION_TYPES); } JsonHandler elementConfiguration = getTypeConfigurationOrThrow( elementType); return new JsonHandler<>(collection -> { ArrayNode arrayNode = JsonUtil.getObjectMapper().createArrayNode(); collection.forEach(element -> arrayNode .add(elementConfiguration.serialize(element))); return arrayNode; }, json -> { /* * Deserialize an empty array of the expected type to reuse * Jackson's logic for creating arbitrary collections. */ @SuppressWarnings("unchecked") FIELDVALUE collection = (FIELDVALUE) JsonUtil.toInstance( JsonUtil.getObjectMapper().createArrayNode(), collectionType); /* * Then deserialize each element according to our json handler and * add it to the collection */ ((ArrayNode) json).forEach(elementJson -> collection .add(elementConfiguration.deserialize(elementJson))); return collection; }); } /** * Gets the optional expiration timeout of this binder. An empty * {@link Optional} is returned if no timeout is set, which means the binder * is not cleared when there are no connected users to the related topic * (this is the default). * * @return the expiration timeout * * @since 3.1 */ @Override public Optional getExpirationTimeout() { return Optional.ofNullable(expirationTimeout); } /** * Sets the expiration timeout of this binder. If set, this binder data is * cleared when {@code expirationTimeout} has passed after the last * connection to the related topic is closed. If set to {@code null}, the * timeout is cancelled. * * @param expirationTimeout * the expiration timeout * * @since 3.1 */ @Override public void setExpirationTimeout(Duration expirationTimeout) { this.expirationTimeout = expirationTimeout; if (formManager != null) { formManager.setExpirationTimeout(expirationTimeout); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy