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