ElementObservableList.java
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2017 the original author or authors.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package griffon.javafx.collections;
019 
020 import javafx.beans.value.ChangeListener;
021 import javafx.beans.value.ObservableValue;
022 import javafx.collections.FXCollections;
023 import javafx.collections.ListChangeListener;
024 import javafx.collections.ObservableList;
025 
026 import javax.annotation.Nonnull;
027 import javax.annotation.Nullable;
028 import java.lang.reflect.InvocationTargetException;
029 import java.lang.reflect.Method;
030 import java.util.ArrayList;
031 import java.util.Collections;
032 import java.util.LinkedHashMap;
033 import java.util.List;
034 import java.util.Map;
035 
036 import static java.util.Objects.requireNonNull;
037 
038 /**
039  @author Andres Almiray
040  @since 2.10.0
041  */
042 public class ElementObservableList<E> extends DelegatingObservableList<E> {
043     public interface ObservableValueContainer {
044         @Nonnull
045         ObservableValue<?>[] observableValues();
046     }
047 
048     public interface ObservableValueExtractor<E> {
049         @Nonnull
050         ObservableValue<?>[] observableValues(@Nullable E instance);
051     }
052 
053     private final Map<E, List<ListenerSubscription>> subscriptions = new LinkedHashMap<>();
054     private final ObservableValueExtractor<E> observableValueExtractor;
055 
056     public ElementObservableList() {
057         this(FXCollections.observableArrayList()new DefaultObservableValueExtractor<>());
058     }
059 
060     public ElementObservableList(@Nonnull ObservableValueExtractor<E> observableValueExtractor) {
061         this(FXCollections.observableArrayList(), observableValueExtractor);
062     }
063 
064     public ElementObservableList(@Nonnull ObservableList<E> delegate) {
065         this(delegate, new DefaultObservableValueExtractor<>());
066     }
067 
068     public ElementObservableList(@Nonnull ObservableList<E> delegate, @Nonnull ObservableValueExtractor<E> observableValueExtractor) {
069         super(delegate);
070         this.observableValueExtractor = requireNonNull(observableValueExtractor, "Argument 'observableValueExtractor' must not be null");
071     }
072 
073     @Override
074     protected void sourceChanged(@Nonnull ListChangeListener.Change<? extends E> c) {
075         while (c.next()) {
076             if (c.wasAdded()) {
077                 c.getAddedSubList().forEach(this::registerListeners);
078             else if (c.wasRemoved()) {
079                 c.getRemoved().forEach(this::unregisterListeners);
080             }
081         }
082         fireChange(c);
083     }
084 
085     private void registerListeners(@Nonnull E element) {
086         if (subscriptions.containsKey(element)) {
087             return;
088         }
089 
090         List<ListenerSubscription> elementSubscriptions = new ArrayList<>();
091         for (ObservableValue<?> observable : observableValueExtractor.observableValues(element)) {
092             elementSubscriptions.add(createChangeListener(element, observable));
093         }
094         subscriptions.put(element, elementSubscriptions);
095     }
096 
097     @Nonnull
098     @SuppressWarnings("unchecked")
099     private ListenerSubscription createChangeListener(@Nonnull final E element, @Nonnull final ObservableValue<?> observable) {
100         final ChangeListener listener = (value, oldValue, newValue-> fireChange(changeFor(element));
101         observable.addListener(listener);
102         return () -> observable.removeListener(listener);
103     }
104 
105     @Nonnull
106     private ListChangeListener.Change<? extends E> changeFor(@Nonnull final E element) {
107         final int position = indexOf(element);
108         final int[] permutations = new int[0];
109 
110         return new ListChangeListener.Change<E>(this) {
111             private boolean invalid = true;
112 
113             @Override
114             public boolean next() {
115                 if (invalid) {
116                     invalid = false;
117                     return true;
118                 }
119                 return false;
120             }
121 
122             @Override
123             public void reset() {
124                 invalid = true;
125             }
126 
127             @Override
128             public int getFrom() {
129                 return position;
130             }
131 
132             @Override
133             public int getTo() {
134                 return position + 1;
135             }
136 
137             @Override
138             public List<E> getRemoved() {
139                 return Collections.emptyList();
140             }
141 
142             @Override
143             protected int[] getPermutation() {
144                 return permutations;
145             }
146 
147             @Override
148             public boolean wasUpdated() {
149                 return true;
150             }
151         };
152     }
153 
154     private void unregisterListeners(@Nonnull E element) {
155         List<ListenerSubscription> registeredSubscriptions = subscriptions.remove(element);
156         if (registeredSubscriptions != null) {
157             registeredSubscriptions.forEach(ListenerSubscription::unsubscribe);
158         }
159     }
160 
161     private interface ListenerSubscription {
162         void unsubscribe();
163     }
164 
165     private static class DefaultObservableValueExtractor<T> implements ObservableValueExtractor<T> {
166         private final Map<Class<?>, List<Method>> observableValueMetadata = new LinkedHashMap<>();
167 
168         @Nonnull
169         @Override
170         public ObservableValue<?>[] observableValues(@Nullable T instance) {
171             if (instance == null) {
172                 return new ObservableValue[0];
173             }
174 
175             if (instance instanceof ElementObservableList.ObservableValueContainer) {
176                 return ((ObservableValueContainerinstance).observableValues();
177             }
178 
179             List<Method> metadata = observableValueMetadata.computeIfAbsent(instance.getClass()this::harvestMetadata);
180 
181             ObservableValue[] observableValues = new ObservableValue[metadata.size()];
182             for (int i = 0; i < observableValues.length; i++) {
183                 try {
184                     observableValues[i(ObservableValuemetadata.get(i).invoke(instance);
185                 catch (IllegalAccessException e) {
186                     throw new IllegalStateException(e);
187                 catch (InvocationTargetException e) {
188                     throw new IllegalStateException(e.getTargetException());
189                 }
190             }
191             return observableValues;
192         }
193 
194         private List<Method> harvestMetadata(@Nonnull Class<?> klass) {
195             List<Method> metadata = new ArrayList<>();
196 
197             for (Method method : klass.getMethods()) {
198                 if (ObservableValue.class.isAssignableFrom(method.getReturnType()) &&
199                     method.getParameterCount() == 0) {
200                     metadata.add(method);
201                 }
202             }
203 
204             return metadata;
205         }
206     }
207 }