Skip to content

Commit

Permalink
Improve documentation of reactive config model
Browse files Browse the repository at this point in the history
  • Loading branch information
Emil Forslund committed Jan 20, 2016
1 parent d1544b6 commit 44e0fa4
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 10 deletions.
Expand Up @@ -56,13 +56,13 @@
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import static javafx.collections.FXCollections.observableMap;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.unmodifiableObservableMap;

/**
*
Expand Down Expand Up @@ -90,8 +90,9 @@ public abstract class AbstractDocumentProperty implements DocumentProperty, HasE
/**
* An internal map of the {@link DocumentProperty Document Properties} that
* have been created by this class. Once a list is associated with a
* particular key, it should never be removed. It should be configured to
* listen to changes in the raw map before being inserted into this map.
* particular key, it should never be removed. A new list should be
* configured to listen to changes in the raw map before being inserted into
* this map.
*/
private final transient ObservableMap<String, ObservableList<DocumentProperty>> documents;

Expand Down Expand Up @@ -157,6 +158,11 @@ protected AbstractDocumentProperty(Map<String, Object> data) {
observableList(new CopyOnWriteArrayList<>(children))
));
}

// Since the key did not exist already, there
// are no listeners that will be triggered by
// this modification.
invalidate();

// An observable list already exists on the
// specified key. Create document views of the
Expand Down Expand Up @@ -201,6 +207,11 @@ protected AbstractDocumentProperty(Map<String, Object> data) {
}

properties.put(change.getKey(), newProperty);

// Since the key did not exist already, there
// are no listeners that will be triggered by
// this modification.
invalidate();
}
}
}
Expand All @@ -209,9 +220,27 @@ protected AbstractDocumentProperty(Map<String, Object> data) {
});
}

/**
* Returns an unmodifiable view of the raw data of this document. To modify
* a value in this map, the appropriate {@code Property} or
* {@code ObservableList} should be used.
* <p>
* These are the methods that should be used for modification:
* <ul>
* <li>{@link #put(String, Object)}
* <li>{@link #stringPropertyOf(String, Supplier)}
* <li>{@link #integerPropertyOf(String, Supplier)}
* <li>{@link #longPropertyOf(String, Supplier)}
* <li>{@link #doublePropertyOf(String, Supplier)}
* <li>{@link #objectPropertyOf(String, Class, Supplier)}
* <li>{@link #observableListOf(String, Class, Supplier)}
* </ul>
*
* @return the raw data
*/
@Override
public final Map<String, Object> getData() {
return config;
public final ObservableMap<String, Object> getData() {
return unmodifiableObservableMap(config);
}

@Override
Expand Down Expand Up @@ -247,9 +276,15 @@ public final Optional<String> getAsString(String key) {
return get(key).map(String.class::cast);
}

/**
* Updates a value in the raw map and marks the specified
* @param key
* @param value
*/
@Override
public final void put(String key, Object value) {
config.put(key, value);
invalidate();
}

@Override
Expand Down Expand Up @@ -313,17 +348,24 @@ public final <T> ObjectProperty<T> objectPropertyOf(String key, Class<T> type, S
* type as {@link #createDocument(java.lang.String, java.util.Map) createDocument}
* generated.
*/
@Override
public final <P extends DocumentProperty, T extends DocumentProperty> ObservableList<T> observableListOf(String key, BiFunction<P, Map<String, Object>, T> constructor) throws SpeedmentException {

// Make sure the component is invalidated if a new key was introduced.
final AtomicBoolean dirty = new AtomicBoolean(false);

// The reason why the results are copied to a separate list before being
// used in the statement below is to guarantee that only one method is
// using the monitor the same tame.
final List<T> existing = new ArrayList<>();

monitor.runWithoutGeneratingEvents(() -> {
try {
DOCUMENT_LIST_TYPE.cast(config.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()))
.stream().map(child -> constructor.apply((P) this, child))
DOCUMENT_LIST_TYPE.cast(config.computeIfAbsent(key, k -> {
dirty.set(true);
return new CopyOnWriteArrayList<>();
})).stream()
.map(child -> constructor.apply((P) this, child))
.forEach(existing::add);
} catch (ClassCastException ex) {
throw new SpeedmentException(
Expand All @@ -344,6 +386,10 @@ public final <P extends DocumentProperty, T extends DocumentProperty> Observable
"Requested an ObservableList on key '" + key +
"' of a different type than 'createDocument' created.", ex
);
} finally {
if (dirty.get()) {
invalidate();
}
}
}

Expand All @@ -356,12 +402,12 @@ public final <P extends DocumentProperty, T extends DocumentProperty> Observable
*/
@Override
public ObservableMap<String, ObservableList<DocumentProperty>> childrenProperty() {
return FXCollections.unmodifiableObservableMap(documents);
return unmodifiableObservableMap(documents);
}

protected DocumentProperty createDocument(String key, Map<String, Object> data) {
return new DefaultDocumentProperty(this, data);
}
}

@SuppressWarnings("unchecked")
private static final Function<Object, List<Object>> UNCHECKED_LIST_CASTER =
Expand All @@ -388,6 +434,10 @@ public final Stream<DocumentProperty> children() {
.values();
}

/**
* Mark this component and all components above it as invalidated so that
* any events observing the tree will know the state has changed.
*/
@Override
public final void invalidate() {
getParent()
Expand Down
122 changes: 121 additions & 1 deletion src/main/java/com/speedment/internal/ui/config/DocumentProperty.java
Expand Up @@ -18,7 +18,10 @@

import com.speedment.config.Document;
import com.speedment.config.db.trait.HasMainInterface;
import com.speedment.exception.SpeedmentException;
import com.speedment.internal.ui.config.trait.HasUiVisibleProperties;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
import java.util.function.IntSupplier;
Expand All @@ -36,27 +39,144 @@
import javafx.collections.ObservableMap;

/**
*
* A special interface marking implementations of {@link Document} that can be
* observed for changes. To observe a specific attribute, call the appropriate
* method for selecting that field.
* <p>
* The following methods exist:
* <ul>
* <li>{@link #stringPropertyOf(String, Supplier)}
* <li>{@link #integerPropertyOf(String, Supplier)}
* <li>{@link #longPropertyOf(String, Supplier)}
* <li>{@link #doublePropertyOf(String, Supplier)}
* <li>{@link #objectPropertyOf(String, Class, Supplier)}
* </ul>
* <p>
* To get an observable view of a specific child type, call
* {@link #observableListOf(String, Class, Supplier)}, or if you want all
* children as they have been exposed so far, call {@link #childrenProperty()}.
* <p>
* As with all JavaFX componenets, the state of this property might not be
* updated immediatly. It is therefore important to use the appropriate property
* getter methods to keep notified about changes to this document.
*
* @author Emil Forslund
*/
public interface DocumentProperty extends Document, HasUiVisibleProperties, HasMainInterface, Observable {

/**
* Wraps the specified String value in a property so that changes to it can
* be observed. Any changes to the returned property will be reflected back
* to the raw map.
*
* @param key the key
* @param ifEmpty a supplier for the initial value should the key not
* already exist
* @return the specified attribute wrapped in a {@code Property}
*/
StringProperty stringPropertyOf(String key, Supplier<String> ifEmpty);

/**
* Wraps the specified int value in a property so that changes to it can
* be observed. Any changes to the returned property will be reflected back
* to the raw map.
*
* @param key the key
* @param ifEmpty a supplier for the initial value should the key not
* already exist
* @return the specified attribute wrapped in a {@code Property}
*/
IntegerProperty integerPropertyOf(String key, IntSupplier ifEmpty);

/**
* Wraps the specified long value in a property so that changes to it can
* be observed. Any changes to the returned property will be reflected back
* to the raw map.
*
* @param key the key
* @param ifEmpty a supplier for the initial value should the key not
* already exist
* @return the specified attribute wrapped in a {@code Property}
*/
LongProperty longPropertyOf(String key, LongSupplier ifEmpty);

/**
* Wraps the specified double value in a property so that changes to it can
* be observed. Any changes to the returned property will be reflected back
* to the raw map.
*
* @param key the key
* @param ifEmpty a supplier for the initial value should the key not
* already exist
* @return the specified attribute wrapped in a {@code Property}
*/
DoubleProperty doublePropertyOf(String key, DoubleSupplier ifEmpty);

/**
* Wraps the specified boolean value in a property so that changes to it can
* be observed. Any changes to the returned property will be reflected back
* to the raw map.
*
* @param key the key
* @param ifEmpty a supplier for the initial value should the key not
* already exist
* @return the specified attribute wrapped in a {@code Property}
*/
BooleanProperty booleanPropertyOf(String key, BooleanSupplier ifEmpty);

<T> ObjectProperty<T> objectPropertyOf(String key, Class<T> type, Supplier<T> ifEmpty);

/**
* Returns an observable list of all the child documents under a specified
* key.
* <p>
* The implementation of the document is governed by the
* {@link #createDocument(java.lang.String, java.util.Map) createDocument}
* method. The specified {@code type} parameter must therefore match the
* implementation created by
* {@link #createDocument(java.lang.String, java.util.Map) createDocument}.
*
* @param <P> the type of this class
* @param <T> the document type
* @param key the key to look at
* @param constructor the constructor to use to create the children views
* @return an observable list of the documents under that key
*
* @throws SpeedmentException if the specified {@code type} is not the same
* type as {@link #createDocument(java.lang.String, java.util.Map) createDocument}
* generated.
*/
<P extends DocumentProperty, T extends DocumentProperty> ObservableList<T> observableListOf(String key, BiFunction<P, Map<String, Object>, T> constructor) throws SpeedmentException;

/**
* Returns a list of all children instantiated using
* {@link #createDocument(String, Map) createDocument} in alphabetical order
* based on the key they belong to. The internal order will be the order
* they have in the list.
*
* @return a stream of the children
*/
@Override
Stream<? extends DocumentProperty> children();

/**
* Returns an unmodifiable map of all the children that this component has
* made visible so far. The {@code ObservableList ObservableLists} contained
* in the map will automatically reflect the children belonging to this
* document, viewed using the constructor supplied to the
* {@link #observableListOf(String, BiFunction)} method when the child key
* was first viewed. If a child key has not been requested before when
* this method is called, the map might not have all child types. These will
* be added as soon as they have been requested, notifying any change
* listeners observing this map.
*
* @return all view of all children made visible so far
*/
ObservableMap<String, ObservableList<DocumentProperty>> childrenProperty();

/**
* Mark this component and all components above it as invalidated so that
* any events observing the tree will know the state has changed.
*/
void invalidate();
}
Expand Up @@ -140,6 +140,7 @@ private <P extends DocumentProperty & HasExpandedProperty> TreeItem<DocumentProp

doc.childrenProperty().addListener((MapChangeListener.Change<? extends String, ? extends ObservableList<DocumentProperty>> change) -> {
if (change.wasAdded()) {
System.out.println("New key '" + change.getKey() + "' was added.");
change.getValueAdded().addListener(onListChange);
change.getValueAdded().stream()
.filter(HasExpandedProperty.class::isInstance)
Expand Down

0 comments on commit 44e0fa4

Please sign in to comment.