/*
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dashbuilder.displayer.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gwt.user.client.ui.Widget;
import org.dashbuilder.common.client.StringUtils;
import org.dashbuilder.common.client.error.ClientRuntimeError;
import org.dashbuilder.dataset.ColumnType;
import org.dashbuilder.dataset.DataColumn;
import org.dashbuilder.dataset.DataSet;
import org.dashbuilder.dataset.ValidationError;
import org.dashbuilder.dataset.client.DataSetReadyCallback;
import org.dashbuilder.dataset.date.DayOfWeek;
import org.dashbuilder.dataset.date.Month;
import org.dashbuilder.dataset.filter.DataSetFilter;
import org.dashbuilder.dataset.group.ColumnGroup;
import org.dashbuilder.dataset.group.DataSetGroup;
import org.dashbuilder.dataset.group.DateIntervalPattern;
import org.dashbuilder.dataset.group.DateIntervalType;
import org.dashbuilder.dataset.group.GroupStrategy;
import org.dashbuilder.dataset.group.Interval;
import org.dashbuilder.dataset.sort.SortOrder;
import org.dashbuilder.displayer.ColumnSettings;
import org.dashbuilder.displayer.DisplayerConstraints;
import org.dashbuilder.displayer.DisplayerSettings;
import org.dashbuilder.displayer.client.formatter.ValueFormatter;
import org.uberfire.client.mvp.UberView;
/**
* Base class for implementing custom displayers.
* <p>Any derived class must implement:
* <ul>
* <li>The draw(), redraw() & close() methods.</li>
* <li>The capture of events coming from the DisplayerListener interface.</li>
* </ul>
*/
public abstract class AbstractDisplayer<V extends AbstractDisplayer.View> implements Displayer {
public interface View<P extends Displayer> extends UberView<P> {
void errorMissingSettings();
void errorMissingHandler();
void showLoading();
void showVisualization();
void clear();
void setId(String id);
void errorDataSetNotFound(String uuid);
void error(ClientRuntimeError error);
void enableRefreshTimer(int seconds);
void cancelRefreshTimer();
}
public interface Formatter {
String formatDate(String pattern, Date d);
Date parseDate(String pattern, String d);
String formatNumber(String pattern, Number n);
String formatDayOfWeek(DayOfWeek dayOfWeek);
String formatMonth(Month month);
}
public interface ExpressionEval {
String evalExpression(String value, String expression);
}
protected DataSet dataSet;
protected DataSetHandler dataSetHandler;
protected DisplayerSettings displayerSettings;
protected DisplayerConstraints displayerConstraints;
protected List<DisplayerListener> listenerList = new ArrayList<DisplayerListener>();
protected Map<String,List<Interval>> columnSelectionMap = new HashMap<String,List<Interval>>();
protected Map<String,ValueFormatter> formatterMap = new HashMap<String, ValueFormatter>();
protected Formatter formatter = null;
protected ExpressionEval evaluator = null;
protected DataSetFilter currentFilter = null;
protected boolean refreshEnabled = true;
protected boolean drawn = false;
@Override
public Widget asWidget() {
return getView().asWidget();
}
/**
* It returns the actual implementation of the View
* <p>- To be provided by the concrete displayer implementation -</p>
*/
public abstract V getView();
/**
* It initializes the constraints this displayer conforms to
* <p>- To be provided by the concrete displayer implementation -</p>
*/
public abstract DisplayerConstraints createDisplayerConstraints();
/**
* The required logic in charge of rendering the visualization
* once the data has been retrieved during a call to draw()
* <p>- To be provided by the concrete displayer implementation -</p>
*/
protected abstract void createVisualization();
/**
* The required logic in charge of updating a visualization
* once the data has been retrieved during a call to redraw()
* <p>- To be provided by the concrete displayer implementation -</p>
*/
protected abstract void updateVisualization();
public DisplayerConstraints getDisplayerConstraints() {
if (displayerConstraints == null) {
displayerConstraints = createDisplayerConstraints();
}
return displayerConstraints;
}
public DisplayerSettings getDisplayerSettings() {
return displayerSettings;
}
public void setDisplayerSettings(DisplayerSettings displayerSettings) {
checkDisplayerSettings(displayerSettings);
this.displayerSettings = displayerSettings;
}
public void checkDisplayerSettings(DisplayerSettings displayerSettings) {
DisplayerConstraints constraints = getDisplayerConstraints();
if (displayerConstraints != null) {
ValidationError error = constraints.check(displayerSettings);
if (error != null) {
throw error;
}
}
}
public DataSetHandler getDataSetHandler() {
return dataSetHandler;
}
public void setDataSetHandler(DataSetHandler dataSetHandler) {
this.dataSetHandler = dataSetHandler;
}
public Formatter getFormatter() {
if (formatter == null) {
formatter = new DisplayerGwtFormatter();
}
return formatter;
}
public void setFormatter(Formatter formatter) {
this.formatter = formatter;
}
public ExpressionEval getEvaluator() {
if (evaluator == null) {
evaluator = new DisplayerGwtExprEval(this);
}
return evaluator;
}
public void setEvaluator(ExpressionEval evaluator) {
this.evaluator = evaluator;
}
public void addListener(DisplayerListener... listeners) {
for (DisplayerListener listener : listeners) {
listenerList.add(listener);
}
}
public String getDisplayerId() {
String id = displayerSettings.getUUID();
if (!StringUtils.isBlank(id)) {
return id;
}
id = displayerSettings.getTitle();
if (!StringUtils.isBlank(id)) {
int hash = id.hashCode();
return Integer.toString(hash < 0 ? hash*-1 : hash);
}
return null;
}
// DRAW & REDRAW
@Override
public boolean isDrawn() {
return drawn;
}
/**
* Draw the displayer by executing first the lookup call to retrieve the target data set
*/
@Override
public void draw() {
if (displayerSettings == null) {
getView().errorMissingSettings();
}
else if (dataSetHandler == null) {
getView().errorMissingHandler();
}
else if (!isDrawn()) {
try {
drawn = true;
getView().showLoading();
beforeLoad();
beforeDataSetLookup();
dataSetHandler.lookupDataSet(new DataSetReadyCallback() {
public void callback(DataSet result) {
try {
dataSet = result;
afterDataSetLookup(result);
createVisualization();
getView().showVisualization();
// Set the id of the container panel so that the displayer can be easily located
// by testing tools for instance.
String id = getDisplayerId();
if (!StringUtils.isBlank(id)) {
getView().setId(id);
}
// Draw done
afterDraw();
} catch (Exception e) {
// Give feedback on any initialization error
showError(new ClientRuntimeError(e));
}
}
public void notFound() {
getView().errorDataSetNotFound(displayerSettings.getDataSetLookup().getDataSetUUID());
}
@Override
public boolean onError(final ClientRuntimeError error) {
showError(error);
return false;
}
});
} catch (Exception e) {
showError(new ClientRuntimeError(e));
}
}
}
/**
* Just reload the data set and make the current displayer to redraw.
*/
@Override
public void redraw() {
if (!isDrawn()) {
draw();
} else {
try {
beforeLoad();
beforeDataSetLookup();
dataSetHandler.lookupDataSet(new DataSetReadyCallback() {
public void callback(DataSet result) {
try {
dataSet = result;
afterDataSetLookup(result);
updateVisualization();
// Redraw done
afterRedraw();
} catch (Exception e) {
// Give feedback on any initialization error
showError(new ClientRuntimeError(e));
}
}
public void notFound() {
String uuid = displayerSettings.getDataSetLookup().getDataSetUUID();
getView().errorDataSetNotFound(uuid);
handleError("Data set not found: " + uuid);
}
@Override
public boolean onError(final ClientRuntimeError error) {
showError(error);
return false;
}
});
} catch (Exception e) {
showError(new ClientRuntimeError(e));
}
}
}
public void showError(ClientRuntimeError error) {
getView().error(error);
handleError(error);
}
/**
* Close the displayer
*/
@Override
public void close() {
getView().clear();
// Close done
afterClose();
}
/**
* Call back method invoked just before the data set lookup is executed.
*/
protected void beforeDataSetLookup() {
}
/**
* Call back method invoked just after the data set lookup is executed.
*/
protected void afterDataSetLookup(DataSet dataSet) {
}
// REFRESH TIMER
@Override
public void setRefreshOn(boolean enabled) {
boolean changed = enabled != refreshEnabled;
refreshEnabled = enabled;
if (changed) {
updateRefreshTimer();
}
}
@Override
public boolean isRefreshOn() {
return refreshEnabled;
}
protected void updateRefreshTimer() {
if (isDrawn()) {
int seconds = displayerSettings.getRefreshInterval();
if (refreshEnabled && seconds > 0) {
getView().enableRefreshTimer(seconds);
} else {
getView().cancelRefreshTimer();
}
}
}
// LIFECYCLE CALLBACKS
protected void beforeLoad() {
for (DisplayerListener listener : listenerList) {
listener.onDataLookup(this);
}
}
protected void afterDraw() {
updateRefreshTimer();
for (DisplayerListener listener : listenerList) {
listener.onDraw(this);
}
}
protected void afterRedraw() {
updateRefreshTimer();
for (DisplayerListener listener : listenerList) {
listener.onRedraw(this);
}
}
protected void afterClose() {
setRefreshOn(false);
for (DisplayerListener listener : listenerList) {
listener.onClose(this);
}
}
public void handleError(final String message) {
handleError(new ClientRuntimeError(message, null));
}
public void handleError(final String message, final Throwable error) {
handleError(new ClientRuntimeError(message, error));
}
public void handleError(final Throwable error) {
handleError(new ClientRuntimeError(error));
}
public void handleError(final ClientRuntimeError error) {
for (DisplayerListener listener : listenerList) {
listener.onError(this, error);
}
}
// CAPTURE EVENTS RECEIVED FROM OTHER DISPLAYERS
@Override
public void onDataLookup(Displayer displayer) {
// Do nothing
}
@Override
public void onDraw(Displayer displayer) {
// Do nothing
}
@Override
public void onRedraw(Displayer displayer) {
// Do nothing
}
@Override
public void onClose(Displayer displayer) {
// Do nothing
}
@Override
public void onError(final Displayer displayer, ClientRuntimeError error) {
// Do nothing
}
@Override
public void onFilterEnabled(Displayer displayer, DataSetGroup groupOp) {
if (displayerSettings.isFilterListeningEnabled()) {
if (dataSetHandler.filter(groupOp)) {
redraw();
}
}
}
@Override
public void onFilterEnabled(Displayer displayer, DataSetFilter filter) {
if (displayerSettings.isFilterListeningEnabled()) {
if (dataSetHandler.filter(filter)) {
redraw();
}
}
}
@Override
public void onFilterReset(Displayer displayer, List<DataSetGroup> groupOps) {
if (displayerSettings.isFilterListeningEnabled()) {
boolean applied = false;
for (DataSetGroup groupOp : groupOps) {
if (dataSetHandler.unfilter(groupOp)) {
applied = true;
}
}
if (applied) {
redraw();
}
}
}
@Override
public void onFilterReset(Displayer displayer, DataSetFilter filter) {
if (displayerSettings.isFilterListeningEnabled()) {
if (dataSetHandler.unfilter(filter)) {
redraw();
}
}
}
// DATA COLUMN VALUES SELECTION, FILTER & NOTIFICATION
/**
* Get the set of columns being filtered.
*/
public Set<String> filterColumns() {
return columnSelectionMap.keySet();
}
/**
* Get the current filter intervals for the given data set column.
*
* @param columnId The column identifier.
* @return A list of intervals.
*/
public List<Interval> filterIntervals(String columnId) {
List<Interval> selected = columnSelectionMap.get(columnId);
if (selected == null) return new ArrayList<Interval>();
return selected;
}
/**
* Get the current filter selected interval indexes for the given data set column.
*
* @param columnId The column identifier.
* @return A list of interval indexes
*/
public List<Integer> filterIndexes(String columnId) {
List<Integer> result = new ArrayList<Integer>();
List<Interval> selected = columnSelectionMap.get(columnId);
if (selected == null) return result;
for (Interval interval : selected) {
result.add(interval.getIndex());
}
return result;
}
/**
* Updates the current filter values for the given data set column.
*
* @param columnId The column to filter for.
* @param row The row selected.
*/
public void filterUpdate(String columnId, int row) {
filterUpdate(columnId, row, null);
}
/**
* Updates the current filter values for the given data set column.
*
* @param columnId The column to filter for.
* @param row The row selected.
* @param maxSelections The number of different selectable values available.
*/
public void filterUpdate(String columnId, int row, Integer maxSelections) {
if (displayerSettings.isFilterEnabled()) {
Interval intervalSelected = dataSetHandler.getInterval(columnId, row);
if (intervalSelected != null) {
List<Interval> selectedIntervals = columnSelectionMap.get(columnId);
if (selectedIntervals == null) {
selectedIntervals = new ArrayList<Interval>();
selectedIntervals.add(intervalSelected);
columnSelectionMap.put(columnId, selectedIntervals);
filterApply(columnId, selectedIntervals);
}
else if (selectedIntervals.contains(intervalSelected)) {
selectedIntervals.remove(intervalSelected);
if (!selectedIntervals.isEmpty()) {
filterApply(columnId, selectedIntervals);
}
else {
filterReset(columnId);
}
}
else {
if (displayerSettings.isFilterSelfApplyEnabled()) {
selectedIntervals = new ArrayList<Interval>();
columnSelectionMap.put(columnId, selectedIntervals);
}
selectedIntervals.add(intervalSelected);
if (maxSelections != null && maxSelections > 0 && selectedIntervals.size() >= maxSelections) {
filterReset(columnId);
}
else {
filterApply(columnId, selectedIntervals);
}
}
}
}
}
/**
* Filter the values of the given column.
*
* @param columnId The name of the column to filter.
* @param intervalList A list of interval selections to filter for.
*/
public void filterApply(String columnId, List<Interval> intervalList) {
if (displayerSettings.isFilterEnabled()) {
// For string column filters, init the group interval selection operation.
DataSetGroup groupOp = dataSetHandler.getGroupOperation(columnId);
groupOp.setSelectedIntervalList(intervalList);
// Notify to those interested parties the selection event.
if (displayerSettings.isFilterNotificationEnabled()) {
for (DisplayerListener listener : listenerList) {
listener.onFilterEnabled(this, groupOp);
}
}
// Drill-down support
if (displayerSettings.isFilterSelfApplyEnabled()) {
dataSetHandler.drillDown(groupOp);
redraw();
}
}
}
/**
* Apply the given filter
*
* @param filter A filter
*/
public void filterApply(DataSetFilter filter) {
if (displayerSettings.isFilterEnabled()) {
this.currentFilter = filter;
// Notify to those interested parties the selection event.
if (displayerSettings.isFilterNotificationEnabled()) {
for (DisplayerListener listener : listenerList) {
listener.onFilterEnabled(this, filter);
}
}
// Drill-down support
if (displayerSettings.isFilterSelfApplyEnabled()) {
dataSetHandler.filter(filter);
redraw();
}
}
}
/**
* Clear any filter on the given column.
*
* @param columnId The name of the column to reset.
*/
public void filterReset(String columnId) {
if (displayerSettings.isFilterEnabled()) {
columnSelectionMap.remove(columnId);
DataSetGroup groupOp = dataSetHandler.getGroupOperation(columnId);
// Notify to those interested parties the reset event.
if (displayerSettings.isFilterNotificationEnabled()) {
for (DisplayerListener listener : listenerList) {
listener.onFilterReset(this, Arrays.asList(groupOp));
}
}
// Apply the selection to this displayer
if (displayerSettings.isFilterSelfApplyEnabled()) {
dataSetHandler.drillUp(groupOp);
redraw();
}
}
}
/**
* Clear any filter.
*/
public void filterReset() {
if (displayerSettings.isFilterEnabled()) {
List<DataSetGroup> groupOpList = new ArrayList<DataSetGroup>();
for (String columnId : columnSelectionMap.keySet()) {
DataSetGroup groupOp = dataSetHandler.getGroupOperation(columnId);
groupOpList.add(groupOp);
}
columnSelectionMap.clear();
// Notify to those interested parties the reset event.
if (displayerSettings.isFilterNotificationEnabled()) {
for (DisplayerListener listener : listenerList) {
if (currentFilter != null) {
listener.onFilterReset(this, currentFilter);
}
listener.onFilterReset(this, groupOpList);
}
}
// Apply the selection to this displayer
if (displayerSettings.isFilterSelfApplyEnabled()) {
boolean applied = false;
if (currentFilter != null) {
if (dataSetHandler.unfilter(currentFilter)) {
applied = true;
}
}
for (DataSetGroup groupOp : groupOpList) {
if (dataSetHandler.drillUp(groupOp)) {
applied = true;
}
}
if (applied) {
redraw();
}
}
if (currentFilter != null) {
currentFilter = null;
}
}
}
// DATA COLUMN SORT
/**
* Set the sort order operation to apply to the data set.
*
* @param columnId The name of the column to sort.
* @param sortOrder The sort order.
*/
public void sortApply(String columnId, SortOrder sortOrder) {
dataSetHandler.sort(columnId, sortOrder);
}
// DATA FORMATTING
public String formatInterval(Interval interval, DataColumn column) {
// Raw values
if (column == null || column.getColumnGroup() == null) {
return interval.getName();
}
// Date interval
String type = interval.getType();
if (StringUtils.isBlank(type)) type = column.getIntervalType();
if (StringUtils.isBlank(type)) type = column.getColumnGroup().getIntervalSize();
DateIntervalType intervalType = DateIntervalType.getByName(type);
if (intervalType != null) {
ColumnSettings columnSettings = displayerSettings.getColumnSettings(column.getId());
String pattern = columnSettings != null ? columnSettings.getValuePattern() : ColumnSettings.getDatePattern(intervalType);
String expression = columnSettings != null ? columnSettings.getValueExpression() : null;
if (pattern == null) {
pattern = ColumnSettings.getDatePattern(intervalType);
}
if (expression == null && column.getColumnGroup().getStrategy().equals(GroupStrategy.FIXED)) {
expression = ColumnSettings.getFixedExpression(intervalType);
}
return formatDate(intervalType,
column.getColumnGroup().getStrategy(),
interval.getName(), pattern, expression);
}
// Label interval
ColumnSettings columnSettings = displayerSettings.getColumnSettings(column);
String expression = columnSettings.getValueExpression();
if (StringUtils.isBlank(expression)) return interval.getName();
return getEvaluator().evalExpression(interval.getName(), expression);
}
public void addFormatter(String columnId, ValueFormatter formatter) {
formatterMap.put(columnId, formatter);
}
public ValueFormatter getFormatter(String columnId) {
return formatterMap.get(columnId);
}
public String formatValue(int row, int column) {
Object value = dataSet.getValueAt(row, column);
DataColumn columnObj = dataSet.getColumnByIndex(column);
ValueFormatter formatter = getFormatter(columnObj.getId());
if (formatter != null) {
return formatter.formatValue(dataSet, row, column);
}
return formatValue(value, columnObj);
}
public String formatValue(Object value, DataColumn column) {
ValueFormatter formatter = getFormatter(column.getId());
if (formatter != null) {
return formatter.formatValue(value);
}
ColumnSettings columnSettings = displayerSettings.getColumnSettings(column);
String pattern = columnSettings.getValuePattern();
String empty = columnSettings.getEmptyTemplate();
String expression = columnSettings.getValueExpression();
if (value == null) {
return empty;
}
// Date grouped columns
DateIntervalType intervalType = DateIntervalType.getByName(column.getIntervalType());
if (intervalType != null) {
ColumnGroup columnGroup = column.getColumnGroup();
return formatDate(intervalType,
columnGroup.getStrategy(),
value.toString(), pattern, expression);
}
// Label grouped columns, aggregations & raw values
else {
ColumnType columnType = column.getColumnType();
if (ColumnType.DATE.equals(columnType)) {
Date d = (Date) value;
return getFormatter().formatDate(pattern, d);
}
else if (ColumnType.NUMBER.equals(columnType)) {
double n = ((Number) value).doubleValue();
if (!StringUtils.isBlank(expression)) {
String r = getEvaluator().evalExpression(value.toString(), expression);
try {
n = Double.parseDouble(r);
} catch (NumberFormatException e) {
return r;
}
}
return getFormatter().formatNumber(pattern, n);
}
else {
if (StringUtils.isBlank(expression)) {
return value.toString();
}
return getEvaluator().evalExpression(value.toString(), expression);
}
}
}
// DATE FORMATTING
protected String formatDate(DateIntervalType type, GroupStrategy strategy, String date, String pattern, String expression) {
if (date == null) {
return null;
}
String str = GroupStrategy.FIXED.equals(strategy) ? formatDateFixed(type, date) : formatDateDynamic(type, date, pattern);
if (StringUtils.isBlank(expression)) {
return str;
}
return getEvaluator().evalExpression(str, expression);
}
protected String formatDateFixed(DateIntervalType type, String date) {
if (date == null) {
return null;
}
int index = Integer.parseInt(date);
if (DateIntervalType.DAY_OF_WEEK.equals(type)) {
DayOfWeek dayOfWeek = DayOfWeek.getByIndex(index);
return getFormatter().formatDayOfWeek(dayOfWeek);
}
if (DateIntervalType.MONTH.equals(type)) {
Month month = Month.getByIndex(index);
return getFormatter().formatMonth(month);
}
return date;
}
protected String formatDateDynamic(DateIntervalType type, String date, String pattern) {
if (date == null) {
return null;
}
Date d = parseDynamicGroupDate(type, date);
return getFormatter().formatDate(pattern, d);
}
protected Date parseDynamicGroupDate(DateIntervalType type, String date) {
String pattern = DateIntervalPattern.getPattern(type);
return getFormatter().parseDate(pattern, date);
}
}