ConfigurationPropertiesRebinder.java

/*
 * Copyright 2012-present the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.cloud.context.properties;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration.RefreshProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.util.ProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Listens for {@link EnvironmentChangeEvent} and rebinds beans that were bound to the
 * {@link Environment} using {@link ConfigurationProperties
 * <code>@ConfigurationProperties</code>}. When these beans are re-bound and
 * re-initialized, the changes are available immediately to any component that is using
 * the <code>@ConfigurationProperties</code> bean.
 *
 * <p>
 * Rebinding a given bean is serialized against other concurrent rebinds of that same
 * bean, so two overlapping refreshes (for example a manual {@link #rebind(String)} call
 * racing with an {@link EnvironmentChangeEvent}-triggered {@link #rebind()}) cannot
 * interleave their destroy/reset/re-initialize steps.
 *
 * <p>
 * To revert properties that were removed from the {@link Environment} to their defaults
 * without exposing those defaults to other threads, a new instance of the bean's class is
 * bound first and its state is then copied onto the live bean. A concurrent reader
 * therefore never sees a bound property reset to its class-level default (for example a
 * {@code null} nested object or a {@code 0}), and getter-only collections and maps are
 * updated in place rather than being cleared and refilled. The bean is still mutated in
 * place, field by field, so a reader can observe a mix of old and new values while a
 * rebind is in progress. Beans that need a consistent view across a refresh should use
 * {@link RefreshScope} instead, which serializes reads against refreshes via a per-bean
 * {@link java.util.concurrent.locks.ReadWriteLock}.
 *
 * <p>
 * Known limitation: a collection property that has a value in the {@link Environment} can
 * be briefly empty during a rebind. The bean is initialized again after the new state has
 * been copied onto it, and Spring Boot's binder updates an existing, modifiable
 * collection by clearing it and then adding the bound elements. That happens inside
 * Spring Boot's binding and cannot be avoided here. Maps are not affected.
 *
 * @author Dave Syer
 * @author Yanming Zhou
 * @see RefreshScope for a deeper and optionally more focused refresh of bean components.
 *
 */
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
		implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

	private static final Log logger = LogFactory.getLog(ConfigurationPropertiesRebinder.class);

	private ConfigurationPropertiesBeans beans;

	private ApplicationContext applicationContext;

	private Map<String, Exception> errors = new ConcurrentHashMap<>();

	private final ConcurrentMap<String, Lock> rebindLocks = new ConcurrentHashMap<>();

	private final Set<String> neverResetNestedTypes;

	private final Set<String> loggedResetFailures = ConcurrentHashMap.newKeySet();

	/**
	 * Safety net against unbounded recursion in {@link #resetProperties}. Cyclic graphs
	 * are already broken by the identity-based {@code visited} set, and graphs built from
	 * unstable (newly-allocated-per-call) instances are broken by
	 * {@link #isStableInstance(BeanWrapper, String, Object)}; this limit only guards
	 * against a gap in either of those checks, or a legitimately very deep object graph.
	 */
	private static final int MAX_RESET_DEPTH = 25;

	public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
		this(beans, Collections.emptySet());
	}

	public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans, RefreshProperties refreshProperties) {
		this(beans, refreshProperties.getNeverResetNestedTypes());
	}

	private ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans, Set<String> neverResetNestedTypes) {
		this.beans = beans;
		this.neverResetNestedTypes = neverResetNestedTypes;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	/**
	 * A map of bean name to errors when instantiating the bean.
	 * @return The errors accumulated since the latest destroy.
	 */
	public Map<String, Exception> getErrors() {
		return this.errors;
	}

	@ManagedOperation
	public void rebind() {
		this.errors.clear();
		for (String name : this.beans.getBeanNames()) {
			rebind(name);
		}
	}

	@ManagedOperation
	public boolean rebind(String name) {
		if (!this.beans.getBeanNames().contains(name)) {
			return false;
		}
		ApplicationContext appContext = this.applicationContext;
		while (appContext != null) {
			if (appContext.containsLocalBean(name)) {
				return rebind(name, appContext);
			}
			else {
				appContext = appContext.getParent();
			}
		}
		return false;
	}

	/**
	 * WARNING: This method rebinds beans from any context in the hierarchy using the main
	 * application context.
	 * @param type bean type to rebind.
	 * @return true, if successful.
	 */
	public boolean rebind(Class type) {
		String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.applicationContext, type);
		if (beanNamesForType.length > 0) {
			String name = beanNamesForType[0];
			if (ScopedProxyUtils.isScopedTarget(name)) {
				name = ScopedProxyUtils.getOriginalBeanName(name);
			}
			return rebind(name, this.applicationContext);
		}
		return false;
	}

	private boolean rebind(String name, ApplicationContext appContext) {
		// Serialize concurrent rebinds of the *same* bean (for example a manual
		// rebind(name) racing with an EnvironmentChangeEvent-triggered rebind()) so
		// their destroy/reset/re-initialize steps cannot interleave on the live bean.
		// This does not protect concurrent readers of the bean; see the class Javadoc.
		Lock lock = this.rebindLocks.computeIfAbsent(name, key -> new ReentrantLock());
		lock.lock();
		try {
			try {
				Object bean = appContext.getBean(name);
				if (bean != null && !AopUtils.isAopProxy(bean)) {
					// A BeanPostProcessor may have replaced the bean with a wrapper after
					// the properties were bound (for example Boot's lazy connection fetch
					// wraps the DataSource). Rebind what the properties were bound to,
					// not
					// the wrapper, which could not be bound to the bean's declared type.
					Object bound = this.beans.getInstance(name);
					if (bound != null) {
						bean = bound;
					}
				}
				if (bean != null) {
					// The class behind every proxy layer, not just the outermost one
					Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
					// TODO: determine a more general approach to fix this.
					// see
					// https://github.com/spring-cloud/spring-cloud-commons/issues/571
					if (getNeverRefreshable().contains(targetClass.getName()) || getNeverRefreshable().contains(name)) {
						return false; // ignore
					}
					if (AopUtils.isAopProxy(bean) && bean instanceof Advised advised) {
						Object target = ProxyUtils.getTargetObject(bean);
						if (target != bean && !targetClass.isInterface()
								&& !Modifier.isAbstract(targetClass.getModifiers())) {
							Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(targetClass);
							Object freshTarget = AopUtils.isAopProxy(freshBean) ? ProxyUtils.getTargetObject(freshBean)
									: freshBean;
							// Keep the advice of every proxy layer
							innermostAdvised(advised).setTargetSource(new SingletonTargetSource(freshTarget));
							appContext.getAutowireCapableBeanFactory().destroyBean(target);
						}
						else {
							Object boundDefaults = bindFreshInstance(target, name, appContext);
							appContext.getAutowireCapableBeanFactory().destroyBean(target);
							resetBeanToDefaults(target, boundDefaults);
							appContext.getAutowireCapableBeanFactory().autowireBean(target);
							appContext.getAutowireCapableBeanFactory().initializeBean(target, name);
						}
					}
					else {
						Object boundDefaults = bindFreshInstance(bean, name, appContext);
						appContext.getAutowireCapableBeanFactory().destroyBean(bean);
						resetBeanToDefaults(bean, boundDefaults);
						appContext.getAutowireCapableBeanFactory().autowireBean(bean);
						appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
					}
					return true;
				}
			}
			catch (RuntimeException e) {
				this.errors.put(name, e);
				throw e;
			}
			catch (Exception e) {
				this.errors.put(name, e);
				throw new IllegalStateException("Cannot rebind to " + name, e);
			}
			return false;
		}
		finally {
			lock.unlock();
		}
	}

	/**
	 * Find the innermost proxy of a, possibly nested, chain of AOP proxies: the one whose
	 * target is not itself a proxy.
	 */
	private Advised innermostAdvised(Advised advised) throws Exception {
		Advised current = advised;
		Object next = current.getTargetSource().getTarget();
		while (AopUtils.isAopProxy(next) && next instanceof Advised nested) {
			current = nested;
			next = current.getTargetSource().getTarget();
		}
		return current;
	}

	/**
	 * Create a new instance of the bean's class and bind it from the {@link Environment},
	 * <em>without touching the live bean</em>. The result is what the live bean should
	 * look like after rebinding: removed properties carry their class-level defaults and
	 * everything else carries its freshly bound value. Applying it to the live bean (see
	 * {@link #resetBeanToDefaults}) therefore never exposes the unbound defaults to
	 * concurrent readers (gh-1727), and a binding failure here leaves the live bean as it
	 * was.
	 * @return the bound instance, or {@code null} if one cannot be produced, in which
	 * case the live bean is simply re-bound in place
	 */
	private Object bindFreshInstance(Object bean, String name, ApplicationContext appContext) {
		Class<?> targetClass = AopUtils.getTargetClass(bean);
		if (!hasDefaultConstructor(targetClass)) {
			// Beans whose no-arg constructor (if any) is not their only constructor
			// (for example constructor-bound beans, beans with required dependencies, or
			// beans with an extra no-arg constructor kept only for a framework's benefit)
			// cannot be instantiated to obtain trustworthy defaults, so the reset is
			// skipped. The bean is still re-bound from the Environment afterwards; only
			// reverting removed properties to their defaults is skipped.
			if (logger.isDebugEnabled()) {
				logger.debug("No default constructor for " + targetClass.getName()
						+ "; skipping property reset before rebinding");
			}
			return null;
		}
		ConfigurationPropertiesBindingPostProcessor binder;
		try {
			binder = appContext.getBean(ConfigurationPropertiesBindingPostProcessor.class);
		}
		catch (BeansException ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("No ConfigurationPropertiesBindingPostProcessor available; skipping property reset of "
						+ targetClass.getName());
			}
			return null;
		}
		Object freshInstance;
		try {
			freshInstance = BeanUtils.instantiateClass(targetClass);
		}
		catch (Exception ex) {
			logger.warn("Cannot create default instance of " + targetClass.getName()
					+ " for reset; skipping property reset", ex);
			return null;
		}
		// Deliberately autowire the throw-away instance, even though it is only used as a
		// source of values. The fresh instance is later copied onto the live bean
		// property by property (see resetProperties), and anything that is injected
		// through a writable property (for example an @Autowired setter) would be null
		// on an un-autowired instance. Copying that null would wipe the live bean's
		// collaborator and leave it null until the live bean is autowired again, which
		// reopens the very window (gh-1727) this method exists to close. With the
		// instance autowired, the copy writes the same collaborator that is already
		// there, which resetProperties skips as "nothing to change". The cost is one
		// extra round of dependency injection per rebind; only autowireBean is called,
		// so injection happens but no initialization callbacks (afterPropertiesSet,
		// @PostConstruct, init methods) run on the instance.
		appContext.getAutowireCapableBeanFactory().autowireBean(freshInstance);
		// Only the binding post-processor runs, so no other post-processor or lifecycle
		// callback sees the throw-away instance. A binding failure propagates before the
		// live bean has been touched.
		binder.postProcessBeforeInitialization(freshInstance, name);
		return freshInstance;
	}

	/**
	 * Copy the state of a freshly bound instance onto the live bean, so that removed
	 * properties do not retain stale values after rebinding.
	 */
	private void resetBeanToDefaults(Object bean, Object boundDefaults) {
		if (boundDefaults != null) {
			resetProperties(bean, boundDefaults, Collections.newSetFromMap(new IdentityHashMap<>()), 0);
		}
	}

	/**
	 * Whether the given type's <em>only</em> declared constructor is a no-argument one,
	 * which is what {@link BeanUtils#instantiateClass(Class)} needs to build a defaults
	 * template that is actually representative of the bean's defaults.
	 * <p>
	 * A no-arg constructor that sits alongside another, parameterized constructor is
	 * deliberately not enough here, regardless of its visibility: some beans declare one
	 * purely for a framework's benefit (for example Jackson deserialization) next to
	 * another constructor that takes collaborators and computes the bean's real defaults
	 * (for example from the current host). Instantiating the type through the no-arg
	 * constructor then produces a template with the wrong defaults - typically
	 * {@code null} or zero-valued fields - for anything the other constructor would have
	 * computed, and resetting the bean to that template before rebinding wipes out values
	 * no property source ever carried (see gh-1733). Only a type whose sole constructor
	 * takes no arguments can be assumed to have one intended, complete way of producing a
	 * default instance.
	 */
	private boolean hasDefaultConstructor(Class<?> type) {
		Constructor<?>[] constructors = type.getDeclaredConstructors();
		return constructors.length == 1 && constructors[0].getParameterCount() == 0;
	}

	private void resetProperties(Object bean, Object defaults, Set<Object> visited, int depth) {
		// Guard against cyclic object graphs and pathologically deep ones so that
		// recursion
		// always terminates; see MAX_RESET_DEPTH.
		if (bean == null || depth > MAX_RESET_DEPTH || !visited.add(bean)) {
			return;
		}
		BeanWrapper target = new BeanWrapperImpl(bean);
		BeanWrapper defaultsWrapper = new BeanWrapperImpl(defaults);
		for (PropertyDescriptor pd : target.getPropertyDescriptors()) {
			String propertyName = pd.getName();
			if ("class".equals(propertyName)) {
				continue;
			}
			try {
				// A property whose declared type the user excluded is left alone whatever
				// its accessors look like: a read-only property is not descended into and
				// a writable one is not overwritten with the default instance's value.
				if (isNeverReset(pd.getPropertyType())) {
					continue;
				}
				if (target.isWritableProperty(propertyName) && defaultsWrapper.isReadableProperty(propertyName)) {
					if (target.isReadableProperty(propertyName)
							&& isNeverReset(target.getPropertyValue(propertyName))) {
						continue;
					}
					Object defaultValue = defaultsWrapper.getPropertyValue(propertyName);
					Object currentValue = target.isReadableProperty(propertyName)
							? target.getPropertyValue(propertyName) : null;
					// Skip the setter call entirely when there's nothing to change: this
					// avoids needlessly invoking setters that don't tolerate being called
					// again with the same value, and setters that reject null are only
					// ever
					// invoked when the property actually needs to become null.
					if (!ObjectUtils.nullSafeEquals(currentValue, defaultValue)) {
						try {
							target.setPropertyValue(propertyName, defaultValue);
						}
						catch (Exception ex) {
							warnCannotReset(bean, propertyName, ex);
						}
					}
				}
				else if (target.isReadableProperty(propertyName) && defaultsWrapper.isReadableProperty(propertyName)) {
					Object value = target.getPropertyValue(propertyName);
					Object defaultValue = defaultsWrapper.getPropertyValue(propertyName);
					if (value instanceof Collection collection) {
						try {
							mergeCollection(collection, defaultValue instanceof Collection c ? c : null);
						}
						catch (UnsupportedOperationException ex) {
							warnCannotReset(bean, propertyName, ex);
						}
					}
					else if (value instanceof Map map) {
						try {
							mergeMap(map, defaultValue instanceof Map m ? m : null);
						}
						catch (UnsupportedOperationException ex) {
							warnCannotReset(bean, propertyName, ex);
						}
					}
					else if (value != null && defaultValue != null && isResettableNestedType(value.getClass())
							&& isStableInstance(target, propertyName, value)) {
						resetProperties(value, defaultValue, visited, depth + 1);
					}
				}
			}
			catch (Exception ex) {
				if (logger.isDebugEnabled()) {
					logger.debug("Failed to reset property '" + propertyName + "' on "
							+ AopUtils.getTargetClass(bean).getName(), ex);
				}
			}
		}
	}

	/**
	 * Make a getter-only collection hold the same elements as {@code source} without ever
	 * passing through an empty state, which a concurrent reader would otherwise observe
	 * (gh-1727). The new elements are added before the stale ones are removed, so a
	 * reader sees the old elements, then old and new, then the new ones.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void mergeCollection(Collection target, Collection source) {
		if (source == null || source.isEmpty()) {
			target.clear();
			return;
		}
		if (target == source || (target instanceof List || target instanceof Set) && target.equals(source)) {
			return;
		}
		if (target instanceof List list) {
			int stale = list.size();
			list.addAll(source);
			list.subList(0, stale).clear();
		}
		else if (target instanceof Set) {
			target.addAll(source);
			target.retainAll(source);
		}
		else {
			// No way to tell old from new elements in other collection types (they may
			// hold duplicates), so fall back to replacing the contents.
			target.clear();
			target.addAll(source);
		}
	}

	/**
	 * Make a getter-only map hold the same entries as {@code source} without ever passing
	 * through an empty state: new and changed entries are put before removed keys are
	 * dropped.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void mergeMap(Map target, Map source) {
		if (source == null || source.isEmpty()) {
			target.clear();
			return;
		}
		if (target == source || target.equals(source)) {
			return;
		}
		target.putAll(source);
		target.keySet().retainAll(source.keySet());
	}

	/**
	 * Whether re-reading the given read-only property returns the very same instance. A
	 * getter that instead returns a new object on every call cannot be holding any state
	 * that a reset would need to affect, so recursing into such a value would only ever
	 * walk a throw-away object graph - one that, for record-style/fluent accessors, may
	 * not even terminate (see gh-1750).
	 */
	private boolean isStableInstance(BeanWrapper target, String propertyName, Object value) {
		try {
			return target.getPropertyValue(propertyName) == value;
		}
		catch (Exception ex) {
			return false;
		}
	}

	/**
	 * Log, at most once per bean type and property, that a property could not be reset to
	 * its default value and may therefore retain a stale value after this and future
	 * refreshes. The full exception is only logged at DEBUG, on every occurrence, to
	 * avoid spamming the log at the default level while still making the failure
	 * discoverable.
	 */
	private void warnCannotReset(Object bean, String propertyName, Exception ex) {
		String targetClassName = AopUtils.getTargetClass(bean).getName();
		if (this.loggedResetFailures.add(targetClassName + "#" + propertyName)) {
			logger.warn("Cannot reset property '" + propertyName + "' on " + targetClassName
					+ " to its default value; it may retain its previous value after this and future refreshes. "
					+ "Enable DEBUG logging for " + ConfigurationPropertiesRebinder.class.getName()
					+ " for the full exception.");
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Failed to reset property '" + propertyName + "' on " + targetClassName, ex);
		}
	}

	/**
	 * Determine whether a nested property value should be recursively reset. Only
	 * user-defined types are descended into. Recursing into JDK or standard API types
	 * (for example {@link javax.net.ssl.SSLContext}) is both unnecessary and unsafe:
	 * their object graphs may be cyclic, which previously led to a
	 * {@link StackOverflowError} (see gh-1698), and they may expose internal collections
	 * or maps that must not be cleared. Additional types can be excluded via the
	 * {@code spring.cloud.refresh.never-reset-nested-types} property.
	 */
	boolean isResettableNestedType(Class<?> type) {
		if (type.isArray() || BeanUtils.isSimpleValueType(type) || isJdkClass(type) || isStandardApiClass(type)) {
			return false;
		}
		return !isNeverReset(type);
	}

	/**
	 * Whether the given value was excluded through the
	 * {@code spring.cloud.refresh.never-reset-nested-types} property.
	 */
	private boolean isNeverReset(Object value) {
		return value != null && isNeverReset(value.getClass());
	}

	/**
	 * Whether the given type was excluded through the
	 * {@code spring.cloud.refresh.never-reset-nested-types} property. The exclusion is
	 * matched against a property's declared type as well as the runtime class of its
	 * value, so that a type the user asked us to leave alone is neither descended into
	 * nor overwritten with the default instance's value, whether or not the value happens
	 * to be an instance of a subtype whose own name does not carry the excluded prefix.
	 */
	private boolean isNeverReset(Class<?> type) {
		if (type == null) {
			return false;
		}
		String typeName = type.getName();
		for (String excluded : this.neverResetNestedTypes) {
			if (typeName.startsWith(excluded)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Whether the given type is provided by the JDK itself. JDK types are loaded either
	 * by the bootstrap class loader (for example everything in {@code java.base}, which
	 * reports a {@code null} class loader) or by the platform class loader (the other
	 * platform modules); application and library types are loaded by the application
	 * class loader.
	 */
	private boolean isJdkClass(Class<?> type) {
		ClassLoader classLoader = type.getClassLoader();
		return classLoader == null || classLoader == ClassLoader.getPlatformClassLoader();
	}

	/**
	 * Whether the given type belongs to a standard API namespace (Jakarta EE or the
	 * legacy {@code javax} packages). These namespaces are reserved for specifications
	 * and, unlike the JDK modules, are loaded by the application class loader, so they
	 * are not detected by {@link #isJdkClass}.
	 */
	private boolean isStandardApiClass(Class<?> type) {
		String packageName = type.getPackageName();
		return packageName.startsWith("jakarta.") || packageName.startsWith("javax.");
	}

	@ManagedAttribute
	public Set<String> getNeverRefreshable() {
		String neverRefresh = this.applicationContext.getEnvironment()
			.getProperty("spring.cloud.refresh.never-refreshable", "com.zaxxer.hikari.HikariDataSource");
		return StringUtils.commaDelimitedListToSet(neverRefresh);
	}

	@ManagedAttribute
	public Set<String> getBeanNames() {
		return new HashSet<>(this.beans.getBeanNames());
	}

	@Override
	public void onApplicationEvent(EnvironmentChangeEvent event) {
		if (this.applicationContext.equals(event.getSource())
				// Backwards compatible
				|| event.getKeys().equals(event.getSource())) {
			rebind();
		}
	}

}