/*****************************************************************************
* Copyright (c) 2015 CEA LIST.
*
* 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@googlemail.com> - Initial API and implementation
*
*****************************************************************************/
package org.eclipse.nebula.widgets.nattable.formula;
import java.util.Collection;
import org.eclipse.nebula.widgets.nattable.command.DisposeCalculatedValueCacheCommandHandler;
import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.formula.command.DisableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.formula.command.DisableFormulaCachingCommandHandler;
import org.eclipse.nebula.widgets.nattable.formula.command.EnableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.formula.command.EnableFormulaCachingCommandHandler;
import org.eclipse.nebula.widgets.nattable.formula.function.AbstractFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionException;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.util.CalculatedValueCache;
import org.eclipse.nebula.widgets.nattable.util.ICalculator;
/**
* {@link IDataProvider} that is able to evaluate formulas. It wraps around a
* {@link IDataProvider} and checks if the requested value is a formula (starts
* with '='). Otherwise the value of the wrapped {@link IDataProvider} is
* returned.
*
* @see FormulaParser
*
* @since 1.4
*/
public class FormulaDataProvider implements IDataProvider {
protected IDataProvider underlyingDataProvider;
protected FormulaParser formulaParser;
protected FormulaErrorReporter errorReporter;
protected boolean formulaEvaluationEnabled = true;
private CalculatedValueCache valueCache;
private ILayer cacheLayer;
private boolean cacheEnabled = false;
/**
*
* @param underlyingDataProvider
* The {@link IDataProvider} that should be wrapped.
*/
public FormulaDataProvider(IDataProvider underlyingDataProvider) {
this(underlyingDataProvider, new FormulaParser(underlyingDataProvider));
}
/**
* This constructor supports the specification of a {@link FormulaParser} to
* customize parsing.
*
* @param underlyingDataProvider
* The {@link IDataProvider} that should be wrapped.
* @param parser
* The {@link FormulaParser} that should be used for formula
* parsing.
*/
public FormulaDataProvider(IDataProvider underlyingDataProvider, FormulaParser parser) {
this.underlyingDataProvider = underlyingDataProvider;
this.formulaParser = parser;
}
@Override
public Object getDataValue(final int columnIndex, final int rowIndex) {
final Object underlying = this.underlyingDataProvider.getDataValue(columnIndex, rowIndex);
if (this.formulaEvaluationEnabled && underlying != null && this.formulaParser.isFunction(underlying.toString())) {
if (this.cacheEnabled && this.valueCache != null) {
return this.valueCache.getCalculatedValue(columnIndex, rowIndex, true, new ICalculator() {
@Override
public Object executeCalculation() {
return processFormula(underlying.toString(), columnIndex, rowIndex);
}
});
} else {
return processFormula(underlying.toString(), columnIndex, rowIndex);
}
}
return underlying;
}
/**
* Process the given formula String by using the internal
* {@link FormulaParser}.
*
* @param formula
* The formula to process.
* @param columnIndex
* The column index of the cell that contains the formula. Needed
* for error handling.
* @param rowIndex
* The row index of the cell that contains the formula. Needed
* for error handling.
* @return The result of the processed formula or an error markup in case an
* error occurred on processing.
*/
protected Object processFormula(String formula, int columnIndex, int rowIndex) {
try {
if (this.errorReporter != null) {
this.errorReporter.clearFormulaError(columnIndex, rowIndex);
}
return this.formulaParser.parseFunction(formula).getValue();
} catch (FunctionException e) {
if (this.errorReporter != null) {
this.errorReporter.addFormulaError(columnIndex, rowIndex, e.getLocalizedMessage());
}
return e.getErrorMarkup();
} catch (Exception e) {
if (this.errorReporter != null) {
this.errorReporter.addFormulaError(columnIndex, rowIndex, e.getLocalizedMessage());
}
return "#ERROR!"; //$NON-NLS-1$
}
}
/**
* Configure the caching behavior of this {@link FormulaDataProvider}.
*
* @param layer
* The {@link ILayer} to which the internal
* {@link CalculatedValueCache} is connected to. Typically the
* {@link DataLayer} to which this {@link FormulaDataProvider} is
* set. If this value is <code>null</code> formula result caching
* can not be enabled because the {@link CalculatedValueCache}
* needs to operate on an {@link ILayer}.
*/
public void configureCaching(ILayer layer) {
if (this.cacheLayer != null) {
this.cacheLayer.unregisterCommandHandler(DisposeResourcesCommand.class);
this.cacheLayer.unregisterCommandHandler(DisableFormulaCachingCommand.class);
this.cacheLayer.unregisterCommandHandler(EnableFormulaCachingCommand.class);
}
this.cacheLayer = layer;
if (layer != null) {
this.valueCache = new CalculatedValueCache(this.cacheLayer, true, true);
this.cacheEnabled = true;
// register command handlers
this.cacheLayer.registerCommandHandler(new DisposeCalculatedValueCacheCommandHandler(this.valueCache));
this.cacheLayer.registerCommandHandler(new DisableFormulaCachingCommandHandler(this));
this.cacheLayer.registerCommandHandler(new EnableFormulaCachingCommandHandler(this));
} else {
this.valueCache = null;
this.cacheEnabled = false;
}
}
/**
* Returns the data value out of the underlying {@link IDataProvider}
* without checking and performing formula evaluation. Needed in order to
* edit formulas in a NatTable or outside the NatTable.
*
* @param columnIndex
* The column index of the requested value.
* @param rowIndex
* The row index of the requested value.
* @return The value of the underlying {@link IDataProvider} without formula
* evaluation.
*/
public Object getNativeDataValue(int columnIndex, int rowIndex) {
return this.underlyingDataProvider.getDataValue(columnIndex, rowIndex);
}
@Override
public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
if (this.valueCache != null) {
// if a value is set we clear the cache
// since we do not know which cells might reference the specified
// cell, we simply clear the whole cache
this.valueCache.clearCache();
}
this.underlyingDataProvider.setDataValue(columnIndex, rowIndex, newValue);
}
@Override
public int getColumnCount() {
return this.underlyingDataProvider.getColumnCount();
}
@Override
public int getRowCount() {
return this.underlyingDataProvider.getRowCount();
}
/**
* @return The underlying {@link IDataProvider}.
*/
protected IDataProvider getUnderlyingDataProvider() {
return this.underlyingDataProvider;
}
/**
* Enable/Disable formula evaluation.
*
* @param enabled
* <code>true</code> to enable formula evaluation,
* <code>false</code> to disable it.
*/
public void setFormulaEvaluationEnabled(boolean enabled) {
this.formulaEvaluationEnabled = enabled;
}
/**
* Enable/Disable formula result caching. Enabling the formula result
* caching means that the parsing and calculation of formulas is performed
* in a background thread. The result is cached to reduce processing time,
* so the rendering is performed faster. Disabling the formula result
* caching means that parsing and calculation of formulas is performed
* always in the current thread which might lead to slower rendering.
*
* @param enabled
* <code>true</code> to enable formula result caching and
* background processing of parsing and calculation,
* <code>false</code> to disable it.
*
* @see FormulaDataProvider#configureCaching(ILayer)
*/
public void setFormulaCachingEnabled(boolean enabled) {
this.cacheEnabled = enabled;
}
/**
* Register a new function that can be evaluated.
*
* @param functionName
* The name of the function that is used in a formula
* @param value
* The type of {@link AbstractFunction} that should be used when
* evaluation a formula that contains the given function.
*/
public void registerFunction(String functionName, Class<? extends AbstractFunction> value) {
this.formulaParser.registerFunction(functionName, value);
}
/**
*
* @return The names of the registered functions that are evaluated by this
* {@link FormulaDataProvider}.
*/
public Collection<String> getRegisteredFunctions() {
return this.formulaParser.getRegisteredFunctions();
}
/**
*
* @return The {@link FormulaParser} that is used by this
* {@link FormulaDataProvider} to parse function strings.
*/
public FormulaParser getFormulaParser() {
return this.formulaParser;
}
/**
* @return The {@link FormulaErrorReporter} that is used to report formula
* errors to the user.
*/
public FormulaErrorReporter getErrorReporter() {
return this.errorReporter;
}
/**
* @param errorReporter
* The {@link FormulaErrorReporter} that should be used to report
* formula errors to the user.
*/
public void setErrorReporter(FormulaErrorReporter errorReporter) {
this.errorReporter = errorReporter;
}
}