/******************************************************************************* * Copyright (c) 2014, 2016 Dirk Fauth and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Dirk Fauth <dirk.fauth@gmail.com> - initial API and implementation * Roman Flueckiger <roman.flueckiger@mac.com - switched to concurrent hash maps to prevent a concurrency issue * Dirk Fauth <dirk.fauth@googlemail.com> - Bug 459246 *******************************************************************************/ package org.eclipse.nebula.widgets.nattable.util; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.eclipse.nebula.widgets.nattable.layer.ILayer; import org.eclipse.nebula.widgets.nattable.layer.event.CellVisualChangeEvent; /** * This class is intended as a value cache that is able to perform calculations * in a background thread. By default it is configured for smooth updates, which * means that on re-calculation of values, the old values will be returned until * the calculation is done. * <p> * The CalculatedValueCache uses implementations of * {@link ICalculatedValueCacheKey} as the key for the value cache. Usually the * internal default implementations for column or row position, or the * column-row coordinates should fit most of the use cases. */ public class CalculatedValueCache implements ICalculatedValueCache { /** * The ILayer this value cache is connected to. Needed to perform cell * update events when background calculation processes are finished. */ private ILayer layer; /** * ExecutorService that is used to create background threads to process * calculations. */ private ExecutorService executor; /** * Cache that contains the calculated values. Introduced for performance * reasons since the calculation could be CPU intensive. * <p> * This cache will receive updates, e.g. gets cleared on data structure * updates, and will be used to determine whether a new calculation is * necessary. */ private Map<ICalculatedValueCacheKey, Object> cache = new ConcurrentHashMap<ICalculatedValueCacheKey, Object>(); /** * Cache copy of the calculated values. * <p> * This cache will be used to return the value to display. If a value was * calculated before it will be returned until it is recalculated. * <p> * Using a cache copy we ensure smooth updates of calculated values as the * prior calculated values will be returned and updated after the new * calculation has finished instead of switching to the default calculation * value on updates. */ private Map<ICalculatedValueCacheKey, Object> cacheCopy = new ConcurrentHashMap<ICalculatedValueCacheKey, Object>(); /** * Flag to specify if the column position should be used as cache key. * <p> * Can be used together with the row position, so the column/row coordinates * will be used as cache key together. */ private final boolean useColumnAsKey; /** * Flag to specify if the row position should be used as cache key. * <p> * Can be used together with the column position, so the column/row * coordinates will be used as cache key together. */ private final boolean useRowAsKey; /** * Flag to specify if the updates on re-calculation should be performed * smoothly or not. If this value is <code>true</code> the values that were * calculated before will be returned until the new value calculation is * done. Otherwise <code>null</code> will be returned until the calculation * is finished. */ private final boolean smoothUpdates; /** * Creates a new CalculatedValueCache for the specified layer that performs * smooth updates of the calculated values. * <p> * Setting one or both key flags to <code>true</code> will enable automatic * cache key resolution dependent on the configuration. Setting both values * to <code>false</code> will leave the developer to use * {@link CalculatedValueCache#getCalculatedValue(int, int, ICalculatedValueCacheKey, boolean, ICalculator)} * as it is not possible to determine the ICalculatedValueCacheKey * automatically. * * @param layer * The layer to which the CalculatedValueCache is connected. * @param useColumnAsKey * Flag to specify if the column position should be used as cache * key. * @param useRowAsKey * Flag to specify if the row position should be used as cache * key. */ public CalculatedValueCache(ILayer layer, boolean useColumnAsKey, boolean useRowAsKey) { this(layer, useColumnAsKey, useRowAsKey, true); } /** * Creates a new CalculatedValueCache for the specified layer. This * constructor additionally allows to specify if the updates of the * calculated values should be performed smoothly or not. That means if a * value needs to be recalculated, on smooth updating the old value will be * returned until the new value is calculated. Non-smooth updates will * return <code>null</code> until the re-calculation is done. * <p> * Setting one or both key flags to <code>true</code> will enable automatic * cache key resolution dependent on the configuration. Setting both values * to <code>false</code> will leave the developer to use * {@link CalculatedValueCache#getCalculatedValue(int, int, ICalculatedValueCacheKey, boolean, ICalculator)} * as it is not possible to determine the ICalculatedValueCacheKey * automatically. * * @param layer * The layer to which the CalculatedValueCache is connected. * @param useColumnAsKey * Flag to specify if the column position should be used as cache * key. * @param useRowAsKey * Flag to specify if the row position should be used as cache * key. * @param smoothUpdates * Flag to specify if the update of the calculated values should * be performed smoothly. */ public CalculatedValueCache(ILayer layer, boolean useColumnAsKey, boolean useRowAsKey, boolean smoothUpdates) { this.layer = layer; this.executor = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors() + 1, Runtime.getRuntime().availableProcessors() + 1, 5000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); ((ThreadPoolExecutor) this.executor).allowCoreThreadTimeOut(true); this.useColumnAsKey = useColumnAsKey; this.useRowAsKey = useRowAsKey; this.smoothUpdates = smoothUpdates; } @Override public Object getCalculatedValue(final int columnPosition, final int rowPosition, boolean calculateInBackground, final ICalculator calculator) { ICalculatedValueCacheKey key = null; if (this.useColumnAsKey && this.useRowAsKey) { key = new CoordinateValueCacheKey(columnPosition, rowPosition); } else if (this.useColumnAsKey && !this.useRowAsKey) { key = new PositionValueCacheKey(columnPosition); } else if (!this.useColumnAsKey && this.useRowAsKey) { key = new PositionValueCacheKey(rowPosition); } else { throw new IllegalStateException( "CalculatedValueCacheKey is configured to not use column or row position. " //$NON-NLS-1$ + "Use getCalculatedValue() with ICalculatedValueCacheKey parameter instead."); //$NON-NLS-1$ } return getCalculatedValue(columnPosition, rowPosition, key, calculateInBackground, calculator); } @Override public Object getCalculatedValue(final int columnPosition, final int rowPosition, final ICalculatedValueCacheKey key, boolean calculateInBackground, final ICalculator calculator) { Object result = null; if (calculateInBackground) { final Object cacheValue = this.cache.get(key); final Object cacheCopyValue = this.cacheCopy.get(key); result = cacheCopyValue; // if the calculated value is not the same as the cache value, we // need to start the calculation process if (cacheCopyValue == null || !cacheValuesEqual(cacheValue, cacheCopyValue)) { // if this CalculatedValueCache is not configured for smooth // updates, return null instead of the previous calculated value if (!this.smoothUpdates) { result = null; } this.executor.execute(new Runnable() { @Override public void run() { Object summaryValue = calculator.executeCalculation(); addToCache(key, summaryValue); // only fire an update event if the new calculated value // is different to the value in the cache copy if (!cacheValuesEqual(summaryValue, cacheCopyValue) && CalculatedValueCache.this.layer != null) { CalculatedValueCache.this.layer.fireLayerEvent(new CellVisualChangeEvent( CalculatedValueCache.this.layer, columnPosition, rowPosition)); } } }); } } else { // Execute the calculation in the same thread to make printing and // exporting work // Note: this could cause a performance leak and should be used // carefully result = calculator.executeCalculation(); addToCache(key, result); } return result; } @Override public void clearCache() { this.cache.clear(); } @Override public void killCache() { this.cache.clear(); this.cacheCopy.clear(); } /** * Adds the given value to the cache and the cache-copy. This way the new * calculated value gets propagated to both cache instances. * * @param key * The key to which the calculated value belongs to. * @param value * The value for the given coordinates to be cached. */ protected void addToCache(ICalculatedValueCacheKey key, Object value) { if (value != null) { this.cache.put(key, value); this.cacheCopy.put(key, value); } else { this.cache.remove(key); this.cacheCopy.remove(key); } } @Override public void dispose() { this.executor.shutdownNow(); } /** * Null-safe equals check. * * @param value1 * The first value. * @param value2 * The second value. * @return <code>true</code> if both values are equal, <code>false</code> if * not. */ private boolean cacheValuesEqual(Object value1, Object value2) { return ((value1 == null && value2 == null) || (value1 != null && value2 != null && value1.equals(value2))); } @Override public void setLayer(ILayer layer) { this.layer = layer; } /** * ICalculatedValueCacheKey that uses either the column or row position as * key. */ class PositionValueCacheKey implements ICalculatedValueCacheKey { private final int position; public PositionValueCacheKey(int position) { this.position = position; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + this.position; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PositionValueCacheKey other = (PositionValueCacheKey) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (this.position != other.position) return false; return true; } private CalculatedValueCache getOuterType() { return CalculatedValueCache.this; } } /** * ICalculatedValueCacheKey that uses the column and row position as key. */ class CoordinateValueCacheKey implements ICalculatedValueCacheKey { private final int columnPosition; private final int rowPosition; public CoordinateValueCacheKey(int columnPosition, int rowPosition) { this.columnPosition = columnPosition; this.rowPosition = rowPosition; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + this.columnPosition; result = prime * result + this.rowPosition; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CoordinateValueCacheKey other = (CoordinateValueCacheKey) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (this.columnPosition != other.columnPosition) return false; if (this.rowPosition != other.rowPosition) return false; return true; } private CalculatedValueCache getOuterType() { return CalculatedValueCache.this; } } }