/* * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates. * * 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 org.jboss.errai.databinding.client.api; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.jboss.errai.common.client.api.Assert; import org.jboss.errai.databinding.client.BindableProxy; import org.jboss.errai.databinding.client.BindableProxyAgent; import org.jboss.errai.databinding.client.BindableProxyFactory; import org.jboss.errai.databinding.client.Binding; import org.jboss.errai.databinding.client.ComponentAlreadyBoundException; import org.jboss.errai.databinding.client.HasPropertyChangeHandlers; import org.jboss.errai.databinding.client.InvalidPropertyExpressionException; import org.jboss.errai.databinding.client.MapBindableProxy; import org.jboss.errai.databinding.client.NonExistingPropertyException; import org.jboss.errai.databinding.client.OneTimeUnsubscribeHandle; import org.jboss.errai.databinding.client.PropertyChangeHandlerSupport; import org.jboss.errai.databinding.client.PropertyChangeUnsubscribeHandle; import org.jboss.errai.databinding.client.PropertyType; import org.jboss.errai.databinding.client.api.handler.property.PropertyChangeEvent; import org.jboss.errai.databinding.client.api.handler.property.PropertyChangeHandler; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import com.google.gwt.user.client.ui.HasValue; import com.google.gwt.user.client.ui.Widget; /** * Provides an API to programmatically bind properties of a data model instance * (any POJO annotated with {@link Bindable}) to UI fields/widgets. The * properties of the model and the UI components will automatically be kept in * sync for as long as they are bound. * * @author Christian Sadilek <csadilek@redhat.com> * @author Max Barkley <mbarkley@redhat.com> */ public class DataBinder<T> implements HasPropertyChangeHandlers { private final PropertyChangeHandlerSupport propertyChangeHandlerSupport = new PropertyChangeHandlerSupport(); private Multimap<String, Binding> bindings = LinkedHashMultimap.create(); private T proxy; private T paused; protected DataBinder() { } /** * Creates a {@link DataBinder} for a new model instance of the provided type * (see {@link #forType(Class)}). * * @param modelType * The bindable type, must not be null. */ private DataBinder(final Class<T> modelType) { this.proxy = BindableProxyFactory.getBindableProxy(Assert.notNull(modelType)); } /** * Creates a {@link DataBinder} for the provided model instance. * * @param model * The instance of a {@link Bindable} type, must not be null. */ private DataBinder(final T model) { this.proxy = BindableProxyFactory.getBindableProxy(Assert.notNull(model)); } /** * Creates a {@link DataBinder} for a new model instance of the provided type. * * @param modelType * The bindable type, must not be null. */ public static <T> DataBinder<T> forType(final Class<T> modelType) { return new DataBinder<>(modelType); } /** * Creates a {@link DataBinder} for a list with models of the provided type. * * @param modelType * The bindable type, must not be null. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static <T> DataBinder<List<T>> forListOfType(final Class<T> modelType) { return new DataBinder<List<T>>((Class) List.class); } /** * Creates a {@link DataBinder} for a map. * * @param propertyTypes * A collection of name-type pairs describing the properties that can be in map models for the returned * binder. Adding other properties to the model of the returned binder will cause * {@link NonExistingPropertyException NonExistingPropertyExceptions} to be thrown. */ public static DataBinder<Map<String, Object>> forMap( final Map<String, PropertyType> propertyTypes ) { final DataBinder<Map<String, Object>> binder = new DataBinder<>(); binder.proxy = new MapBindableProxy(propertyTypes); return binder; } /** * Creates a {@link DataBinder} for the provided model instance. * * @param model * The instance of a {@link Bindable} type, must not be null. */ public static <T> DataBinder<T> forModel(final T model) { return new DataBinder<>(model); } /** * Binds the provided component to the specified property of the model instance * associated with this {@link DataBinder}. If the provided component already * participates in another binding managed by this {@link DataBinder}, a * {@link ComponentAlreadyBoundException} will be thrown. * * @param component * The UI component the model instance should be bound to, must not be * null. * @param property * The name of the model property that should be used for the * binding, following Java bean conventions. Chained (nested) * properties are supported and must be dot (.) delimited (e.g. * customer.address.street). Must not be null. * @return the same {@link DataBinder} instance to support call chaining. * @throws NonExistingPropertyException * If the {@code model} does not have a property with the given * name. * @throws InvalidPropertyExpressionException * If the provided property chain expression is invalid. * @throws ComponentAlreadyBoundException * If the provided {@code component} is already bound to a property of * the model. */ public DataBinder<T> bind(final Object component, final String property) { return bind(component, property, null); } /** * Binds the provided component to the specified property of the model instance * associated with this {@link DataBinder}. If the provided component already * participates in another binding managed by this {@link DataBinder}, a * {@link ComponentAlreadyBoundException} will be thrown. * * @param component * The UI component the model instance should be bound to, must not be * null. * @param property * The name of the model property that should be used for the * binding, following Java bean conventions. Chained (nested) * properties are supported and must be dot (.) delimited (e.g. * customer.address.street). Must not be null. * @param converter * The converter to use for the binding, null if default conversion * should be used (see {@link Convert#getConverter(Class, Class)} or * {@link Convert#identityConverter(Class)} for possible arguments). * @return the same {@link DataBinder} instance to support call chaining. * @throws NonExistingPropertyException * If the {@code model} does not have a property with the given * name. * @throws InvalidPropertyExpressionException * If the provided property chain expression is invalid. * @throws ComponentAlreadyBoundException * If the provided {@code component} is already bound to a property of * the model. */ public DataBinder<T> bind(final Object component, final String property, @SuppressWarnings("rawtypes") final Converter converter) { bind(component, property, converter, StateSync.FROM_MODEL); return this; } /** * Binds the provided component to the specified property of the model instance * associated with this {@link DataBinder}. If the provided component already * participates in another binding managed by this {@link DataBinder}, a * {@link ComponentAlreadyBoundException} will be thrown. * * @param component * The UI component the model instance should be bound to, must not be * null. * @param property * The name of the model property that should be used for the * binding, following Java bean conventions. Chained (nested) * properties are supported and must be dot (.) delimited (e.g. * customer.address.street). Must not be null. * @param converter * The converter to use for the binding, null if default conversion * should be used (see {@link Convert#getConverter(Class, Class)} or * {@link Convert#identityConverter(Class)} for possible arguments). * @param initialState * Specifies the origin of the initial state of both model and UI * component. Null if no initial state synchronization should be carried * out. * @return the same {@link DataBinder} instance to support call chaining. * @throws NonExistingPropertyException * If the {@code model} does not have a property with the given * name. * @throws InvalidPropertyExpressionException * If the provided property chain expression is invalid. * @throws ComponentAlreadyBoundException * If the provided {@code component} is already bound to a property of * the model. */ public DataBinder<T> bind(final Object component, final String property, @SuppressWarnings("rawtypes") final Converter converter, final StateSync initialState) { return bind(component, property, converter, initialState, false); } /** * Binds the provided component to the specified property of the model instance * associated with this {@link DataBinder}. If the provided component already * participates in another binding managed by this {@link DataBinder}, a * {@link ComponentAlreadyBoundException} will be thrown. * * @param component * The UI component the model instance should be bound to, must not be * null. * @param property * The name of the model property that should be used for the * binding, following Java bean conventions. Chained (nested) * properties are supported and must be dot (.) delimited (e.g. * customer.address.street). Must not be null. * @param converter * The converter to use for the binding, null if default conversion * should be used (see {@link Convert#getConverter(Class, Class)} or * {@link Convert#identityConverter(Class)} for possible arguments). * @param initialState * Specifies the origin of the initial state of both model and UI * component. Null if no initial state synchronization should be carried * out. * @param bindOnKeyUp * A boolean value that allows models bound to text-based components to * be updated on a {@link com.google.gwt.event.dom.client.KeyUpEvent} * as well as the default * {@link com.google.gwt.event.logical.shared.ValueChangeEvent} * @return the same {@link DataBinder} instance to support call chaining. * @throws NonExistingPropertyException * If the {@code model} does not have a property with the given * name. * @throws InvalidPropertyExpressionException * If the provided property chain expression is invalid. * @throws ComponentAlreadyBoundException * If the provided {@code component} is already bound to a property of * the model. */ public DataBinder<T> bind(final Object component, final String property, @SuppressWarnings("rawtypes") final Converter converter, final StateSync initialState, final boolean bindOnKeyUp) { Assert.notNull(component); Assert.notNull(property); if (!(proxy instanceof BindableProxy<?>)) { proxy = BindableProxyFactory.getBindableProxy(Assert.notNull(proxy)); } final Binding binding = getAgent().bind(component, property, converter, bindOnKeyUp, initialState); bindings.put(property, binding); return this; } /** * Unbinds all widgets bound to the specified model property by previous calls * to {@link #bind(HasValue, Object, String)}. This method has no effect if * the specified property was never bound. * * @param property * The name of the property (or a property chain) to unbind, Must not * be null. * * @return the same {@link DataBinder} instance to support call chaining. * @throws InvalidPropertyExpressionException * If the provided property chain expression is invalid. */ public DataBinder<T> unbind(final String property) { for (final Binding binding : bindings.get(property)) { getAgent().unbind(binding); } bindings.removeAll(property); if (bindings.isEmpty()) { // Proxies without bindings will be removed from the cache to make sure // the garbage collector can do its job (see // BindableProxyFactory#removeCachedProxyForModel). We throw away the // reference to the proxy to force a new lookup in case this data binder // will be reused. unwrapProxy(); } return this; } /** * Unbinds all widgets bound by previous calls to * {@link #bind(HasValue, Object, String)} and all * {@link PropertyChangeHandler handlers} bound by previous calls to * {@link #addPropertyChangeHandler(PropertyChangeHandler)}. * * @return the same {@link DataBinder} instance to support call chaining. */ public DataBinder<T> unbind() { return unbind(true); } private DataBinder<T> unbind(final boolean clearBindings) { for (final Binding binding : bindings.values()) { getAgent().unbind(binding); } if (clearBindings) { bindings.clear(); } clearModelHandlers(); // Proxies without bindings will be removed from the cache to make sure the // garbage collector can do its job (see // BindableProxyFactory#removeCachedProxyForModel). We throw away the // reference to the proxy to force a new lookup in case this data binder // will be reused. unwrapProxy(); return this; } private void clearModelHandlers() { getAgent().clearModelHandlers(); } /** * Returns the model instance associated with this {@link DataBinder}. * * @return The model instance which has to be used in place of the provided * model (see {@link #forModel(Object)} and {@link #forType(Class)}) * if changes should be automatically synchronized with the UI. If * this binder has been {@link #pause() paused} then the this returns * the paused model that may not be synchronized with the UI. */ public T getModel() { ensureProxied(); return (paused == null) ? proxy : paused; } /** * Returns the working model instance associated with this {@link DataBinder}. * * @return The model instance which has to be used in place of the provided * model (see {@link #forModel(Object)} and {@link #forType(Class)}) * if changes should be automatically synchronized with the UI. Unlike * {@link #getModel()} this method always returns a model that is * sychronized to the UI, even if this binder has been paused. */ public T getWorkingModel() { ensureProxied(); return proxy; } /** * Changes the underlying model instance. The existing bindings stay intact * but only affect the new model instance. The previously associated model * instance will no longer be kept in sync with the UI. The bound UI widgets * will be updated based on the new model state. Bindings will be resumed if * they are currently paused. * * @param model * The instance of a {@link Bindable} type, must not be null. * @return The model instance which has to be used in place of the provided * model (see {@link #forModel(Object)} and {@link #forType(Class)}) * if changes should be automatically synchronized with the UI (also * accessible using {@link #getModel()}). */ public T setModel(final T model) { return setModel(model, StateSync.FROM_MODEL); } /** * Changes the underlying model instance. The existing bindings stay intact * but only affect the new model instance. The previously associated model * instance will no longer be kept in sync with the UI. Bindings will be * resumed if they are currently paused. * * @param model * The instance of a {@link Bindable} type, must not be null. * @param initialState * Specifies the origin of the initial state of both model and UI * widget. * @return The model instance which has to be used in place of the provided * model (see {@link #forModel(Object)} and {@link #forType(Class)}) * if changes should be automatically synchronized with the UI (also * accessible using {@link #getModel()}). */ public T setModel(final T model, final StateSync initialState) { return setModel(model, initialState, false); } /** * Changes the underlying model instance. The existing bindings stay intact * but only affect the new model instance. The previously associated model * instance will no longer be kept in sync with the UI. Bindings will be * resumed if they are currently paused. * * @param model * The instance of a {@link Bindable} type, must not be null. * @param initialState * Specifies the origin of the initial state of both model and UI * widget. * @param fireChangeEvents * Specifies whether or not {@link PropertyChangeEvent}s should be * fired as a consequence of the model change. * @return The model instance which has to be used in place of the provided * model (see {@link #forModel(Object)} and {@link #forType(Class)}) * if changes should be automatically synchronized with the UI (also * accessible using {@link #getModel()}). */ @SuppressWarnings("unchecked") public T setModel(final T model, final StateSync initialState, final boolean fireChangeEvents) { Assert.notNull(model); final BindableProxy<T> newProxy; final StateSync newInitState = Optional.ofNullable(initialState).orElse(StateSync.FROM_MODEL); if (model instanceof BindableProxy) { newProxy = (BindableProxy<T>) model; } else { newProxy = (BindableProxy<T>) BindableProxyFactory.getBindableProxy(model); } newProxy.getBindableProxyAgent().mergePropertyChangeHandlers(propertyChangeHandlerSupport); if (fireChangeEvents) { newProxy.getBindableProxyAgent().fireChangeEvents(getAgent(), initialState); } if (newProxy != this.proxy) { // unbind the old proxy unbind(false); } // replay all bindings final Multimap<String, Binding> bindings = LinkedHashMultimap.create(); for (final Binding b : this.bindings.values()) { // must be checked before unbind() removes the handlers final boolean bindOnKeyUp = b.needsKeyUpBinding(); newProxy.getBindableProxyAgent().unbind(b); bindings.put(b.getProperty(), newProxy.getBindableProxyAgent() .bind(b.getComponent(), b.getProperty(), b.getConverter(), bindOnKeyUp, newInitState)); } this.paused = null; this.bindings = bindings; this.proxy = (T) newProxy; return this.proxy; } /** * Returns the widgets currently bound to the provided model property (see * {@link #bind(Widget, String)}). * * @param property * The name of the property (or a property chain). Must not be null. * @return the list of widgets currently bound to the provided property or an * empty list if no widget was bound to the property. */ public List<Object> getComponents(final String property) { Assert.notNull(property); final List<Object> widgets = new ArrayList<>(); for (final Binding binding : bindings.get(property)) { widgets.add(binding.getComponent()); } return widgets; } /** * Returns a set of the currently bound property names. * * @return all bound properties, or an empty set if no properties have been * bound. */ public Set<String> getBoundProperties() { return bindings.keySet(); } /** * Pauses all bindings. The model and UI fields are no longer kept in sync * until either {@link #resume(StateSync)} or {@link #setModel(Object)} is * called. This method has no effect if the bindings are already paused. */ @SuppressWarnings("unchecked") public void pause() { if (paused != null) return; final T paused = proxy; final T clone = (T) ((BindableProxy<?>) proxy).deepUnwrap(); setModel(clone); this.paused = paused; } /** * Resumes the previously paused bindings (see {@link #pause()}) and carries * out state synchronization to catch up on changes that happened in the * meantime. This method has no effect if {@link #pause()} was never called. * * @param resumeState * the state to resume from. Must not be null. */ public void resume(final StateSync resumeState) { if (paused == null) return; Assert.notNull(resumeState); setModel(paused, resumeState); } /** * @return true iff {@link #pause()} was called and there have been no calls since to {@link #resume(StateSync)} or * {@link #setModel(Object, StateSync, boolean)}. */ public boolean isPaused() { return (paused != null); } @Override public PropertyChangeUnsubscribeHandle addPropertyChangeHandler(final PropertyChangeHandler<?> handler) { propertyChangeHandlerSupport.addPropertyChangeHandler(handler); final PropertyChangeUnsubscribeHandle agentUnsubHandle = getAgent().addPropertyChangeHandler(handler); return new OneTimeUnsubscribeHandle() { @Override public void doUnsubscribe() { agentUnsubHandle.unsubscribe(); } }; } @Override public <P> PropertyChangeUnsubscribeHandle addPropertyChangeHandler(final String property, final PropertyChangeHandler<P> handler) { propertyChangeHandlerSupport.addPropertyChangeHandler(property, handler); final PropertyChangeUnsubscribeHandle agentUnsubHandle = getAgent().addPropertyChangeHandler(property, handler); return new OneTimeUnsubscribeHandle() { @Override public void doUnsubscribe() { agentUnsubHandle.unsubscribe(); } }; } @SuppressWarnings("unchecked") private BindableProxyAgent<T> getAgent() { ensureProxied(); return ((BindableProxy<T>) this.proxy).getBindableProxyAgent(); } @SuppressWarnings("unchecked") private void unwrapProxy() { if (proxy instanceof BindableProxy<?>) { proxy = (T) ((BindableProxy<T>) proxy).unwrap(); } } private void ensureProxied() { if (!(proxy instanceof BindableProxy<?>)) { proxy = BindableProxyFactory.getBindableProxy(Assert.notNull(proxy)); } } /** * For adding nested bindings. This method must be public, but should not be called by end users. */ public void addBinding(final String property, final Binding binding) { bindings.put(property, binding); } }