/** * * Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved. * * 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 com.speedment.tool.config; import com.speedment.common.function.FloatSupplier; import com.speedment.common.function.OptionalBoolean; import com.speedment.common.mapstream.MapStream; import com.speedment.runtime.config.Document; import static com.speedment.runtime.config.util.DocumentUtil.toStringHelper; import com.speedment.runtime.core.util.OptionalUtil; import com.speedment.tool.config.component.DocumentPropertyComponent; import com.speedment.tool.config.util.NumericProperty; import com.speedment.tool.config.util.SimpleNumericProperty; import static java.util.Collections.newSetFromMap; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.OptionalLong; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.*; import java.util.stream.Stream; import javafx.beans.InvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.FloatProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import static javafx.collections.FXCollections.observableList; import static javafx.collections.FXCollections.observableMap; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; /** * * @param <THIS> the type of the implementing class * * @author Emil Forslund * @since 2.3.0 */ public abstract class AbstractDocumentProperty<THIS extends AbstractDocumentProperty<? super THIS>> implements DocumentProperty { private final Map<String, Object> config; private final transient ObservableMap<String, Property<?>> properties; private final transient ObservableMap<String, ObservableList<AbstractDocumentProperty<?>>> children; /** * Invalidation listeners required by the {@code Observable} interface. */ private final transient Set<InvalidationListener> listeners; protected AbstractDocumentProperty() { this.config = new ConcurrentHashMap<>(); this.properties = observableMap(new ConcurrentHashMap<>()); this.children = observableMap(new ConcurrentHashMap<>()); this.listeners = newSetFromMap(new ConcurrentHashMap<>()); } @Override public final Map<String, Object> getData() { return config; } @Override @Deprecated public final void put(String key, Object val) { throw new UnsupportedOperationException( "Observable config documents does not support the put()-operation " + "directly. Instead you should request the appropriate property or " + "observable list for the specific key and modify it." ); } @Override public final Optional<Object> get(String key) { @SuppressWarnings("unchecked") final Property<Object> prop = (Property<Object>) properties.get(key); if (prop == null) { return Optional.empty(); } else { return Optional.ofNullable(prop.getValue()); } } @Override public final OptionalBoolean getAsBoolean(String key) { final BooleanProperty prop = (BooleanProperty) properties.get(key); if (prop == null) { return OptionalBoolean.empty(); } else { return OptionalBoolean.ofNullable(prop.getValue()); } } @Override public final OptionalLong getAsLong(String key) { final LongProperty prop = (LongProperty) properties.get(key); if (prop == null) { return OptionalLong.empty(); } else { return OptionalUtil.ofNullable(prop.getValue()); } } @Override public final OptionalDouble getAsDouble(String key) { final DoubleProperty prop = (DoubleProperty) properties.get(key); if (prop == null) { return OptionalDouble.empty(); } else { return OptionalUtil.ofNullable(prop.getValue()); } } @Override public final OptionalInt getAsInt(String key) { final IntegerProperty prop = (IntegerProperty) properties.get(key); if (prop == null) { return OptionalInt.empty(); } else { return OptionalUtil.ofNullable(prop.getValue()); } } @Override public final Optional<String> getAsString(String key) { final StringProperty prop = (StringProperty) properties.get(key); if (prop == null) { return Optional.empty(); } else { return Optional.ofNullable(prop.getValue()); } } @Override public final StringProperty stringPropertyOf(String key, Supplier<String> ifEmpty) { return (StringProperty) properties.computeIfAbsent(key, k -> prepare(k, new SimpleStringProperty(), ifEmpty.get())); } @Override public final BooleanProperty booleanPropertyOf(String key, BooleanSupplier ifEmpty) { return (BooleanProperty) properties.computeIfAbsent(key, k -> prepare(k, new SimpleBooleanProperty(), ifEmpty.getAsBoolean())); } @Override public final IntegerProperty integerPropertyOf(String key, IntSupplier ifEmpty) { return numericPropertyOf(key, ifEmpty::getAsInt, NumericProperty::asIntegerProperty); } @Override public final LongProperty longPropertyOf(String key, LongSupplier ifEmpty) { return numericPropertyOf(key, ifEmpty::getAsLong, NumericProperty::asLongProperty); } @Override public final DoubleProperty doublePropertyOf(String key, DoubleSupplier ifEmpty) { return numericPropertyOf(key, ifEmpty::getAsDouble, NumericProperty::asDoubleProperty); } @Override public final FloatProperty floatPropertyOf(String key, FloatSupplier ifEmpty) { return numericPropertyOf(key, ifEmpty::getAsFloat, NumericProperty::asFloatProperty); } private <T extends Property<? extends Number>> T numericPropertyOf(String key, Supplier<? extends Number> ifEmpty, Function<NumericProperty, T> wrapper) { final NumericProperty property = (NumericProperty) properties .computeIfAbsent(key, k -> prepare(k, new SimpleNumericProperty(), ifEmpty.get()) ); return wrapper.apply(property); } @Override public final <T extends DocumentProperty> ObservableList<T> observableListOf(String key) { @SuppressWarnings("unchecked") final ObservableList<T> list = (ObservableList<T>) children.computeIfAbsent(key, k -> addListeners(k, observableList(new CopyOnWriteArrayList<>())) ); return list; } @Override @SuppressWarnings("unchecked") public final ObservableMap<String, ObservableList<DocumentProperty>> childrenProperty() { return (ObservableMap<String, ObservableList<DocumentProperty>>) (ObservableMap<String, ?>) children ; } @Override public final Stream<? extends DocumentProperty> children() { return MapStream.of(children) .sortedByKey(Comparator.naturalOrder()) .flatMapValue(ObservableList::stream) .values(); } @Override @Deprecated public final <P extends Document, T extends Document> Stream<T> children(String key, BiFunction<P, Map<String, Object>, T> constructor) { throw new UnsupportedOperationException("children() shall not be called from a Property"); } @Override public final void invalidate() { listeners.forEach(l -> l.invalidated(this)); getParent().map(DocumentProperty.class::cast) .ifPresent(DocumentProperty::invalidate); } @Override public final void addListener(InvalidationListener listener) { listeners.add(listener); } @Override public final void removeListener(InvalidationListener listener) { listeners.remove(listener); } @Override public String toString() { return toStringHelper(this); } /** * An overridable method used to get the full key path with the specified * trail. This is used to locate the appropriate constructor. * * @param key the key to end with (can be null) * @return the constructor */ protected abstract List<String> keyPathEndingWith(String key); /** * Creates a new child on the specified key with the specified data and * returns it. This method can be overriden by subclasses to create better * implementations. * <p> * <b>Warning!</b> This method is only intended to be called internally and does * not properly configure created children in the responsive model. * * @param documentPropertyComponent the documentPropertyComponent instance * @param key the key to create the child on * @return the created child */ protected final DocumentProperty createChild(DocumentPropertyComponent documentPropertyComponent, String key) { return documentPropertyComponent .getConstructor(keyPathEndingWith(key)) .create(this); } /** * Adds a listener to the specified property so that changes to it are * reflected down to the source map. * * @param <T> the type of the property * @param key the key of the property * @param property the property to listen to * @param initialValue the initial value of this property * @return the same property but with listener attached */ private <T> Property<T> prepare(String key, Property<T> property, T initialValue) { final ChangeListener<T> change = (ob, oldValue, newValue) -> { if (newValue != null) { config.put(key, newValue); } else { config.remove(key); } invalidate(); }; property.setValue(initialValue); property.addListener(change); change.changed(property, null, initialValue); return property; } /** * Adds a listener to the specified list so that changes to it are reflected * down to the source map. * * @param key the key where the list is located * @param list the list to add listeners to * @return the same list but with listener attached */ private ObservableList<AbstractDocumentProperty<?>> addListeners(String key, ObservableList<AbstractDocumentProperty<?>> list) { // When an observable children list under a specific key is // modified, the new children must be inserted into the source // equivalent as well. list.addListener((ListChangeListener.Change<? extends DocumentProperty> listChange) -> { while (listChange.next()) { if (listChange.wasAdded()) { // Find or create a children list in the source map // for the specified key @SuppressWarnings("unchecked") final List<Map<String, Object>> source = (List<Map<String, Object>>) config.computeIfAbsent( key, k -> new CopyOnWriteArrayList<>() ); // Add a reference to the map for every child listChange.getAddedSubList().stream() .map(DocumentProperty::getData) // Exactly the same map as is used in the property .forEachOrdered(source::add); } if (listChange.wasRemoved()) { @SuppressWarnings("unchecked") final List<Map<String, Object>> source = (List<Map<String, Object>>) config.computeIfAbsent( key, k -> new CopyOnWriteArrayList<>() ); listChange.getRemoved().stream() .map(DocumentProperty::getData) .forEach(removed -> source.removeIf(e -> e == removed) // Identity ); } } invalidate(); }); return list; } }