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 ((ObservableValueContainer) instance).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] = (ObservableValue) metadata.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 }
|