/* * Copyright (c) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 David Berkman * * This file is part of the SmallMind Code Project. * * The SmallMind Code Project is free software, you can redistribute * it and/or modify it under either, at your discretion... * * 1) The terms of GNU Affero General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * ...or... * * 2) The terms of the Apache License, Version 2.0. * * The SmallMind Code Project is distributed in the hope that it will * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License or Apache License for more details. * * You should have received a copy of the GNU Affero General Public License * and the Apache License along with the SmallMind Code Project. If not, see * <http://www.gnu.org/licenses/> or <http://www.apache.org/licenses/LICENSE-2.0>. * * Additional permission under the GNU Affero GPL version 3 section 7 * ------------------------------------------------------------------ * If you modify this Program, or any covered work, by linking or * combining it with other code, such other code is not for that reason * alone subject to any of the requirements of the GNU Affero GPL * version 3. */ package org.smallmind.javafx.extras; import java.util.Map; import java.util.NavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import jfxtras.util.PlatformUtil; import org.smallmind.scribe.pen.LoggerManager; public class ConsolidatingChangeListener<T> implements ChangeListener<T>, Comparable<ConsolidatingChangeListener<?>> { private static final CountDownLatch stopLatch = new CountDownLatch(1); private static final ConcurrentSkipListMap<ConsolidatingKey, LooseChange<?>> LOOSE_CHANGE_MAP = new ConcurrentSkipListMap<>(); private final ChangeListener<T> innerChangeListener; private final long consolidationTimeMillis; private int generation; static { Thread thread = new Thread(new ConsolidationWorker()); thread.setDaemon(true); thread.start(); } public ConsolidatingChangeListener (long consolidationTimeMillis, ChangeListener<T> innerChangeListener) { this.consolidationTimeMillis = consolidationTimeMillis; this.innerChangeListener = innerChangeListener; } private ChangeListener<T> getInnerChangeListener () { return innerChangeListener; } private synchronized int getGeneration () { return generation; } @Override public synchronized final void changed (ObservableValue<? extends T> observableValue, T initialValue, T currentValue) { LOOSE_CHANGE_MAP.put(new ConsolidatingKey<>(this, ++generation, consolidationTimeMillis), new LooseChange<>(observableValue, initialValue, currentValue)); } @Override public int compareTo (ConsolidatingChangeListener<?> listener) { return hashCode() - listener.hashCode(); } @Override protected void finalize () { stopLatch.countDown(); } private static class ConsolidationWorker implements Runnable { @Override public void run () { try { while (!stopLatch.await(50, TimeUnit.MILLISECONDS)) { NavigableMap<ConsolidatingKey, LooseChange<?>> expiredKeyMap; if (!(expiredKeyMap = LOOSE_CHANGE_MAP.headMap(new ConsolidatingKey())).isEmpty()) { Map.Entry<ConsolidatingKey, LooseChange<?>> entry; while ((entry = expiredKeyMap.pollFirstEntry()) != null) { synchronized (entry.getKey().getListener()) { if (entry.getKey().getGeneration() == entry.getKey().getListener().getGeneration()) { final ConsolidatingKey key = entry.getKey(); final LooseChange<?> change = entry.getValue(); PlatformUtil.runAndWait(new Runnable() { @Override public void run () { key.getListener().getInnerChangeListener().changed(change.getObservableValue(), change.getInitialValue(), change.getCurrentValue()); } }); } } } } } } catch (InterruptedException interruptedException) { LoggerManager.getLogger(ConsolidatingChangeListener.class).error(interruptedException); } } } private static class ConsolidatingKey<U> implements Comparable<ConsolidatingKey<U>> { private final ConsolidatingChangeListener<U> listener; private final long expiration; private final int generation; private ConsolidatingKey () { this(null, 0, 0); } private ConsolidatingKey (ConsolidatingChangeListener<U> listener, int generation, long consolidationTimeMillis) { this.listener = listener; this.generation = generation; expiration = System.currentTimeMillis() + consolidationTimeMillis; } private ConsolidatingChangeListener<?> getListener () { return listener; } private int getGeneration () { return generation; } private long getExpiration () { return expiration; } @Override public int compareTo (ConsolidatingKey key) { int comparison; if ((comparison = Long.compare(expiration, key.getExpiration())) == 0) { return (listener == null) ? ((key.getListener() == null) ? 0 : -1) : ((key.getListener() == null) ? 1 : listener.compareTo(key.getListener())); } return comparison; } } private static class LooseChange<U> { private final ObservableValue<? extends U> observableValue; private final U initialValue; private final U currentValue; private LooseChange (ObservableValue<? extends U> observableValue, U initialValue, U currentValue) { this.observableValue = observableValue; this.initialValue = initialValue; this.currentValue = currentValue; } private ObservableValue<? extends U> getObservableValue () { return observableValue; } private U getInitialValue () { return initialValue; } private U getCurrentValue () { return currentValue; } } }