package org.codefx.libfx.control.properties; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import javafx.collections.ObservableMap; /** * A builder for a {@code ControlPropertyListener}. This is no type on its own as explained in * {@link ControlPropertyListenerHandle}. Such a handle is returned by this builder. * <p> * It is best created by calling {@link ControlProperties#on(ObservableMap)} with the control's property map as an * argument. It is necessary to set a key (with {@link #forKey(Object)}) and a processor function for the value (with * {@link #processValue(Consumer)}) before calling {@link #buildDetached()}. * <p> * Specifying the value's type with {@link #forValueType(Class)} is optional. If it is done, the built listener will use * it to check the type of the value before casting it to the type accepted by the value processor. If those types do * not match, this prevents {@link ClassCastException} (which would otherwise be caught and silently ignored). If that * case occurs frequently, specifying the type to allow the check will improve performance considerably. * </p> * <h2>Example</h2> * <p> * A typical use looks like this: * * <pre> * ControlProperties.<Boolean> on(control.getProperties()) * .forKey("visible") * .processValue(this::setVisibility) * .buildDetached(); * </pre> * * @param <T> * the type of values which the listener processes */ public class ControlPropertyListenerBuilder<T> { // #begin FIELDS /** * The properties which will be observed. */ private final ObservableMap<Object, Object> properties; /** * The key to which the listener will listen; must no be null by the time {@link #buildDetached()} is called. */ private Object key; /** * The processor of the key's values; must no be null by the time {@link #buildDetached()} is called. */ private Consumer<? super T> valueProcessor; /** * The type of value which the listener processes */ private Optional<Class<T>> valueType; // #end FIELDS // #begin CONSTRUCTION & SETTING VALUES /** * Creates a new builder. * * @param properties * the properties which will be observed by the built listener */ private ControlPropertyListenerBuilder(ObservableMap<Object, Object> properties) { Objects.requireNonNull(properties, "The argument 'properties' must not be null."); this.properties = properties; this.valueType = Optional.empty(); } /** * Creates a builder for a {@link ControlPropertyListenerHandle} which observes the specified property map. * <p> * Note that it is often necessary to explicitly specify the type parameter {@code T} like so: * * <pre> * ControlProperties.<String> on(...) * </pre> * * @param <T> * the type of values which the listener processes * @param properties * the {@link ObservableMap} holding the properties * @return a {@link ControlPropertyListenerBuilder} */ public static <T> ControlPropertyListenerBuilder<T> on(ObservableMap<Object, Object> properties) { return new ControlPropertyListenerBuilder<T>(properties); } /** * Sets the key. This must be called before {@link #buildDetached()}. * * @param key * the key the built listener will observe * @return this builder instance for fluent API */ public ControlPropertyListenerBuilder<T> forKey(Object key) { Objects.requireNonNull(key, "The argument 'key' must not be null."); this.key = key; return this; } /** * Sets the type of the values which the built listener will process. Used to type check before calling the * {@link #processValue(Consumer) valueProcessor}. * <p> * This type is optional. See the class comment on {@link ControlPropertyListenerBuilder this builder} for details. * * @param valueType * the type of values the built listener will process * @return this builder instance for fluent API */ public ControlPropertyListenerBuilder<T> forValueType(Class<T> valueType) { Objects.requireNonNull(valueType, "The argument 'valueType' must not be null."); this.valueType = Optional.of(valueType); return this; } /** * Sets the processor for the key's values. This must be called before {@link #buildAttached()}. * * @param valueProcessor * the {@link Consumer} for the key's values * @return this builder instance for fluent API */ public ControlPropertyListenerBuilder<T> processValue(Consumer<? super T> valueProcessor) { Objects.requireNonNull(valueProcessor, "The argument 'valueProcessor' must not be null."); this.valueProcessor = valueProcessor; return this; } // #end CONSTRUCTION & SETTING VALUES // #begin BUILD /** * Creates a new property listener according to the arguments specified before and * {@link ControlPropertyListenerHandle#attach() attaches} it. * * @return a {@link ControlPropertyListenerHandle}; initially attached * @see #buildDetached() */ public ControlPropertyListenerHandle buildAttached() { ControlPropertyListenerHandle listener = buildDetached(); listener.attach(); return listener; } /** * Creates a new property listener according to the arguments specified before. * <p> * Note that this builder is not yet attached to the map! This can be done by calling * {@link ControlPropertyListenerHandle#attach() attach()} on the returned instance. * * @return a {@link ControlPropertyListenerHandle}; initially detached * @see #buildAttached() */ public ControlPropertyListenerHandle buildDetached() { checkFields(); if (valueType.isPresent()) return new TypeCheckingControlPropertyListenerHandle<T>(properties, key, valueType.get(), valueProcessor); else return new CastingControlPropertyListenerHandle<T>(properties, key, valueProcessor); } /** * Checks whether the fields are valid so they can be used to {@link #buildDetached() build} a listener. */ private void checkFields() { if (key == null) throw new IllegalStateException("Set a key with 'forKey' before calling 'build'."); if (valueProcessor == null) throw new IllegalStateException("Set a value processor with 'processValue' before calling 'build'."); // value type is optional, so no checks } // #end BUILD }