/*
* Sonar, open source software quality management tool.
* Copyright (C) 2009 SonarSource
* mailto:contact AT sonarsource DOT com
*
* Sonar is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* Sonar 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Sonar; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.plugins.timeline.client;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.*;
import com.google.gwt.visualization.client.AbstractDataTable.ColumnType;
import com.google.gwt.visualization.client.DataTable;
import com.google.gwt.visualization.client.VisualizationUtils;
import com.google.gwt.visualization.client.visualizations.AnnotatedTimeLine;
import com.google.gwt.visualization.client.visualizations.AnnotatedTimeLine.Options;
import com.google.gwt.visualization.client.visualizations.AnnotatedTimeLine.ScaleType;
import org.sonar.api.web.gwt.client.widgets.LoadingLabel;
import org.sonar.gwt.JsonUtils;
import org.sonar.gwt.ui.Page;
import org.sonar.wsclient.gwt.AbstractCallback;
import org.sonar.wsclient.gwt.AbstractListCallback;
import org.sonar.wsclient.gwt.Sonar;
import org.sonar.wsclient.services.*;
import java.util.*;
public class GwtTimeline extends Page {
public static final String GWT_ID = "org.sonar.plugins.timeline.GwtTimeline";
public static final List<String> SUPPORTED_METRIC_TYPES = Arrays.asList("INT", "FLOAT", "PERCENT", "MILLISEC");
public static final int DEFAULT_HEIGHT = 480;
public static final String DEFAULT_METRICS_KEY = "sonar.timeline.defaultmetrics";
public static final String DEFAULT_METRICS_VALUE = "ncloc,violations_density,coverage";
private SortedSet<Metric> metrics = null;
private String[] defaultMetrics = null;
private ListBox metricsListBox1 = new ListBox();
private ListBox metricsListBox2 = new ListBox();
private ListBox metricsListBox3 = new ListBox();
private List<ListBox> metricsListBoxes = null;
private CheckBox singleScaleCheckBox = new CheckBox("Single scale");
private SimplePanel tlPanel = null;
private VerticalPanel panel;
private Map<String, Metric> loadedMetrics = new HashMap<String, Metric>();
private Resource resource;
private DataTable dataTable;
@Override
protected Widget doOnResourceLoad(Resource resource) {
panel = new VerticalPanel();
panel.add(new LoadingLabel());
this.resource = resource;
load();
return panel;
}
private void load() {
Sonar.getInstance().find(PropertyQuery.createForKey(DEFAULT_METRICS_KEY), new AbstractCallback<Property>() {
@Override
protected void doOnResponse(Property result) {
defaultMetrics = result.getValue().split(",");
loadMetrics();
}
@Override
protected void doOnError(int errorCode, String errorMessage) {
defaultMetrics = DEFAULT_METRICS_VALUE.split(",");
loadMetrics();
}
});
}
private void loadMetrics() {
Sonar.getInstance().findAll(MetricQuery.all(), new AbstractListCallback<Metric>() {
@Override
protected void doOnResponse(List<Metric> result) {
for (Metric metric : result) {
if (isSupported(metric)) {
loadedMetrics.put(metric.getKey(), metric);
}
}
metrics = filterAndOrderMetrics(result);
metricsListBoxes = Arrays.asList(metricsListBox1, metricsListBox2, metricsListBox3);
loadListBox(metricsListBox1, defaultMetrics.length > 0 ? defaultMetrics[0] : null);
loadListBox(metricsListBox2, defaultMetrics.length > 1 ? defaultMetrics[1] : null);
loadListBox(metricsListBox3, defaultMetrics.length > 2 ? defaultMetrics[2] : null);
ChangeHandler metricSelection = new ChangeHandler() {
public void onChange(ChangeEvent event) {
if (!allMetricsUnSelected() && !sameMetricsSelection()) {
loadTimeline();
}
}
};
for (ListBox metricLb : metricsListBoxes) {
metricLb.addChangeHandler(metricSelection);
}
singleScaleCheckBox.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
renderDataTable(dataTable);
}
});
loadVisualizationApi();
}
private void loadListBox(ListBox metricsLb, String selectedKey) {
metricsLb.setStyleName("small");
metricsLb.addItem("<none>", "");
int index = 1;
for (Metric metric : metrics) {
metricsLb.addItem(metric.getName(), metric.getKey());
if (selectedKey != null && metric.getKey().equals(selectedKey.trim())) {
metricsLb.setSelectedIndex(index);
}
index++;
}
}
private Boolean allMetricsUnSelected() {
for (ListBox metricLb : metricsListBoxes) {
if (getSelectedMetric(metricLb) != null) {
return false;
}
}
return true;
}
private boolean sameMetricsSelection() {
List<Metric> selected = new ArrayList<Metric>();
for (ListBox metricLb : metricsListBoxes) {
Metric metric = getSelectedMetric(metricLb);
if (metric != null) {
if (selected.contains(metric)) {
return true;
}
selected.add(metric);
}
}
return false;
}
});
}
private void loadVisualizationApi() {
Runnable onLoadCallback = new Runnable() {
public void run() {
render();
loadTimeline();
}
};
VisualizationUtils.loadVisualizationApi(onLoadCallback, AnnotatedTimeLine.PACKAGE);
}
private static SortedSet<Metric> filterAndOrderMetrics(Collection<Metric> metrics) {
TreeSet<Metric> ordered = new TreeSet<Metric>(new Comparator<Metric>() {
public int compare(Metric o1, Metric o2) {
return o1.getName().compareTo(o2.getName());
}
});
for (Metric metric : metrics) {
if (isSupported(metric)) {
ordered.add(metric);
}
}
return ordered;
}
private static boolean isSupported(Metric metric) {
return SUPPORTED_METRIC_TYPES.contains(metric.getType()) && !metric.getHidden();
}
private void loadTimeline() {
lockMetricsList(true);
tlPanel.clear();
tlPanel.add(new LoadingLabel());
new TimelineLoader(resource.getKey(), getSelectedMetrics()) {
@Override
void noData() {
renderNoData();
}
@Override
void data(String[] metrics, TimeMachine timemachine, List<Event> events) {
dataTable = getDataTable(metrics, timemachine, events);
renderDataTable(dataTable);
}
};
}
private void renderDataTable(DataTable table) {
if (table != null && table.getNumberOfRows() > 0) {
Element content = DOM.getElementById("content");
int width = content.getClientWidth() > 0 ? content.getClientWidth() : 800;
Widget toRender = new AnnotatedTimeLine(table, createOptions(), width + "px", GwtTimeline.DEFAULT_HEIGHT + "px");
renderTimeline(toRender);
} else {
renderNoData();
}
}
private void renderNoData() {
renderTimeline(new HTML("<p>No data</p>"));
}
private void renderTimeline(Widget toRender) {
lockMetricsList(false);
tlPanel.clear();
tlPanel.add(toRender);
}
private DataTable getDataTable(String[] metrics, TimeMachine timeMachine, List<Event> events) {
DataTable table = DataTable.create();
table.addColumn(ColumnType.DATE, "d", "Date");
for (String metric : metrics) {
table.addColumn(ColumnType.NUMBER, loadedMetrics.get(metric).getName(), metric);
}
table.addColumn(ColumnType.STRING, "e", "Event");
for (TimeMachineCell cell : timeMachine.getCells()) {
int rowIndex = table.addRow();
table.setValue(rowIndex, 0, cell.getDate());
for (int i = 0; i < metrics.length; i++) {
Double value = JsonUtils.getAsDouble((JSONValue) cell.getValues()[i]);
if (value != null) {
table.setValue(rowIndex, i + 1, value);
}
}
}
for (Event event : events) {
int rowIndex = table.addRow();
String eventStr = event.getName();
if (event.getDescription() != null) {
eventStr += " : " + event.getDescription();
}
table.setValue(rowIndex, 0, event.getDate());
table.setValue(rowIndex, metrics.length + 1, eventStr);
}
return table;
}
private void lockMetricsList(boolean locked) {
for (ListBox metricLb : metricsListBoxes) {
metricLb.setEnabled(!locked);
}
}
private String[] getSelectedMetrics() {
List<String> metrics = new ArrayList<String>();
for (ListBox metricLb : metricsListBoxes) {
Metric metric = getSelectedMetric(metricLb);
if (metric != null) {
// inverting metrics
metrics.add(0, metric.getKey());
}
}
return metrics.toArray(new String[metrics.size()]);
}
private Metric getSelectedMetric(ListBox metricsLb) {
String selected = metricsLb.getValue(metricsLb.getSelectedIndex());
return selected.length() > 0 ? loadedMetrics.get(selected) : null;
}
private void render() {
HorizontalPanel hPanel = new HorizontalPanel();
Label label = new Label("Metrics:");
label.setStyleName("note");
hPanel.add(label);
for (ListBox metricLb : metricsListBoxes) {
hPanel.add(new HTML(" "));
hPanel.add(metricLb);
}
hPanel.add(singleScaleCheckBox);
VerticalPanel vPanel = new VerticalPanel();
vPanel.add(hPanel);
tlPanel = new SimplePanel();
vPanel.add(tlPanel);
displayView(vPanel);
}
private void displayView(Widget widget) {
panel.clear();
panel.add(widget);
}
private Options createOptions() {
Options options = Options.create();
options.setAllowHtml(true);
options.setDisplayAnnotations(true);
options.setDisplayAnnotationsFilter(true);
options.setAnnotationsWidth(15);
options.setOption("fill", 15);
options.setOption("thickness", 2);
resetNumberFormats();
int selectedCols = 0;
for (ListBox metricLb : metricsListBoxes) {
if (getSelectedMetric(metricLb) != null) {
setNumberFormats(selectedCols++, getNumberFormat(getSelectedMetric(metricLb)));
}
}
options.setOption("numberFormats", getNumberFormats());
if (!singleScaleCheckBox.getValue()) {
int[] scaledCols = new int[selectedCols];
for (int i = 0; i < selectedCols; i++) {
scaledCols[i] = i;
}
options.setScaleType(ScaleType.ALLFIXED);
options.setScaleColumns(scaledCols);
}
return options;
}
private String getNumberFormat(Metric metric) {
return metric.getType().equals("PERCENT") ? "0.0" : "0.##";
}
private native JavaScriptObject getNumberFormats()
/*-{
return this.numberFormats;
}-*/;
private native void resetNumberFormats()
/*-{
this.numberFormats = {};
}-*/;
private native void setNumberFormats(int key, String numberFormat)
/*-{
this.numberFormats[key] = numberFormat;
}-*/;
}