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