/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 ro.nextreports.server.web.dashboard.chart;
import java.util.Map;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.OnLoadHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.EmptyPanel;
import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.resource.JavaScriptResourceReference;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.util.string.StringValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import ro.nextreports.engine.exporter.exception.NoDataFoundException;
import ro.nextreports.server.domain.Chart;
import ro.nextreports.server.domain.DrillEntityContext;
import ro.nextreports.server.domain.Entity;
import ro.nextreports.server.exception.NotFoundException;
import ro.nextreports.server.report.next.NextUtil;
import ro.nextreports.server.service.ChartService;
import ro.nextreports.server.service.DashboardService;
import ro.nextreports.server.util.ChartUtil;
import ro.nextreports.server.web.NextServerApplication;
import ro.nextreports.server.web.dashboard.Widget;
import ro.nextreports.server.web.dashboard.drilldown.DrillDownWidget;
/**
* @author Mihai Dinca-Panaitescu
*/
public class ChartRendererPanel extends GenericPanel<Chart> {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(ChartRendererPanel.class);
private OnClickChartAjaxBehavior onClickChartAjaxBehavior;
private DrillEntityContext drillContext;
private Map<String,Object> urlQueryParameters;
private String PARAM = "Param";
private String isHTML5String = "";
private WebMarkupContainer container;
private WebMarkupContainer error;
private IModel<Chart> model;
private ChartWidget widget;
private boolean zoom;
private String width;
private String height;
private final ResourceReference NEXT_JS = new JavaScriptResourceReference(ChartHTML5Panel.class, NextServerApplication.NEXT_CHARTS_JS);
@SpringBean
private ChartService chartService;
@SpringBean
private DashboardService dashboardService;
public ChartRendererPanel(String id, ChartWidget widget, DrillEntityContext drillContext, boolean zoom) {
this(id, new Model<Chart>((Chart)widget.getEntity()), widget, drillContext, zoom, null, null, null);
}
public ChartRendererPanel(String id, ChartWidget widget, DrillEntityContext drillContext, boolean zoom, Map<String,Object> urlQueryParameters) {
this(id, new Model<Chart>((Chart)widget.getEntity()), widget, drillContext, zoom, null, null, urlQueryParameters);
}
public ChartRendererPanel(String id, ChartWidget widget, DrillEntityContext drillContext, boolean zoom, String width, String height) {
this(id, new Model<Chart>((Chart)widget.getEntity()), widget, drillContext, zoom, width, height, null);
}
public ChartRendererPanel(String id, ChartWidget widget, DrillEntityContext drillContext, boolean zoom, String width, String height, Map<String,Object> urlQueryParameters) {
this(id, new Model<Chart>((Chart)widget.getEntity()), widget, drillContext, zoom, width, height, urlQueryParameters);
}
public ChartRendererPanel(String id, IModel<Chart> model, DrillEntityContext drillContext, boolean zoom) {
this(id, model, null, drillContext, zoom, null, null, null);
}
public ChartRendererPanel(String id, IModel<Chart> model, DrillEntityContext drillContext, boolean zoom, Map<String,Object> urlQueryParameters) {
this(id, model, null, drillContext, zoom, null, null, urlQueryParameters);
}
public ChartRendererPanel(String id, IModel<Chart> model, DrillEntityContext drillContext, boolean zoom, String width, String height) {
this(id, model, null, drillContext, zoom, width, height, null);
}
public ChartRendererPanel(String id, IModel<Chart> model, DrillEntityContext drillContext, boolean zoom, String width, String height, Map<String,Object> urlQueryParameters) {
this(id, model, null, drillContext, zoom, width, height, urlQueryParameters);
}
private ChartRendererPanel(String id, IModel<Chart> model, ChartWidget widget, DrillEntityContext drillContext, boolean zoom) {
this(id, model, widget, drillContext, zoom, null, null, null);
}
private ChartRendererPanel(String id, final IModel<Chart> model, ChartWidget widget, final DrillEntityContext drillContext, boolean zoom, String width, String height, Map<String,Object> urlQueryParameters) {
super(id, model);
this.drillContext = drillContext;
this.urlQueryParameters = urlQueryParameters;
this.model = model;
this.widget = widget;
this.zoom = zoom;
this.width = width;
this.height = height;
if ((drillContext != null) && !drillContext.isLast()) {
onClickChartAjaxBehavior = new OnClickChartAjaxBehavior() {
private static final long serialVersionUID = 1L;
@Override
public void onClickChart(AjaxRequestTarget target, String value) {
try {
// x values pattern
String pattern = NextUtil.getNextChart(model.getObject()).getXPattern();
ChartRendererPanel.this.onClickChart(target, value, pattern);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
};
add(onClickChartAjaxBehavior);
}
container = new WebMarkupContainer("chartContainer");
container.setOutputMarkupId(true);
container.add(new EmptyPanel("chart"));
// add this class to have the same height when we drill inside a chart
// remove it when an error occurs (see below)
container.add(AttributeAppender.append("class", "dragbox-content-chart zoom"));
add(container);
error = new WebMarkupContainer("errorContainer");
error.setOutputMarkupId(true);
error.add(new EmptyPanel("error"));
add(error);
add(new HTML5Behavior());
}
class ChartModel extends LoadableDetachableModel<String> {
private static final long serialVersionUID = 1L;
private ChartWidget widget;
private Throwable error;
private IModel<Chart> model;
private boolean isHTML5;
public ChartModel(IModel<Chart> model, ChartWidget widget, boolean isHTML5) {
this.model = model;
this.widget = widget;
this.isHTML5 = isHTML5;
}
@Override
public String load() {
error = null;
try {
// test for unsupported flash types (implemented only with HTML5)
if ((!isHTML5) && ChartUtil.unsupportedFlashType(widget.getChartType())) {
throw new Exception("Chart Type '" + widget.getChartType() + "' is not supported in flash mode. Please select a type that is not in the following: " + ChartUtil.FLASH_UNSUPPORTED);
}
if (drillContext == null) {
String jsonData;
if (widget == null) {
jsonData = chartService.getJsonData(model.getObject(), urlQueryParameters, isHTML5);
} else {
jsonData = chartService.getJsonData(widget, urlQueryParameters, isHTML5);
}
jsonData = updateJsonData(jsonData);
return jsonData;
}
// get onClickJavaScript
String onClickJavaScript = null;
if (onClickChartAjaxBehavior != null) {
//onClickJavaScript = "alert('hello')";
onClickJavaScript = onClickChartAjaxBehavior.getOnClickJavaScript(isHTML5);
}
LOG.debug("onClickJavaScript = " + onClickJavaScript);
drillContext.setDrillLink(onClickJavaScript);
// get current chart
Chart chart = getModelObject();
String jsonData = null;
if (drillContext.getDrillParameterValues().isEmpty() && (widget != null)) {
// use chart widget (instead of chart) because it also keeps the chart settings!!
chart = (Chart)widget.getEntity();
jsonData = chartService.getJsonData(widget, drillContext, urlQueryParameters, isHTML5);
} else {
jsonData = chartService.getJsonData(chart, drillContext, urlQueryParameters, isHTML5);
}
if (chart != null) {
LOG.debug("current chart = " + chart.getName());
} else {
LOG.debug("current chart = null");
}
LOG.debug("jsonData = " + jsonData);
jsonData = updateJsonData(jsonData);
return jsonData;
} catch (Throwable t) {
container.add(AttributeAppender.remove("class"));
LOG.error(t.getMessage(), t);
error = t;
return null;
}
}
public Throwable getError() {
return error;
}
public boolean hasError() {
return error != null;
}
public String updateJsonData(String jsonData) throws Exception {
boolean adjustFont = false;
if (urlQueryParameters != null) {
Object adjustTextFontSize = urlQueryParameters.get("adjustableTextFontSize");
if (adjustTextFontSize != null) {
adjustFont = (Boolean)adjustTextFontSize;
}
}
if (adjustFont || zoom) {
ObjectMapper mapper = new ObjectMapper();
ObjectNode json = (ObjectNode)mapper.readTree(jsonData);
json.put("adjustableTextFontSize",true);
jsonData = json.toString();
}
return jsonData;
}
}
private class HTML5Behavior extends AbstractDefaultAjaxBehavior {
private static final long serialVersionUID = 1L;
public HTML5Behavior() {
super();
}
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
StringBuilder javaScript = new StringBuilder();
javaScript.append("var data = isCanvasEnabled();");
javaScript.append("console.log(data);");
javaScript.append("return { '" + PARAM + "': data }");
attributes.getDynamicExtraParameters().add(javaScript);
}
@Override
public void renderHead(Component component, IHeaderResponse response) {
super.renderHead(component, response);
//include js file
response.render(JavaScriptHeaderItem.forReference(NEXT_JS));
response.render(OnLoadHeaderItem.forScript(getCallbackFunctionBody()));
}
@Override
protected void respond(AjaxRequestTarget target) {
String param = getRequest().getRequestParameters().getParameterValue(PARAM).toString();
StringValue widgetIdString = getRequest().getRequestParameters().getParameterValue("id");
if (widgetIdString != null) {
String widgetId = widgetIdString.toString();
if (widgetId != null) {
// for iframe we get widget id from url!!!
try {
Widget loadWidget = dashboardService.getWidgetById(widgetId);
if (loadWidget instanceof ChartWidget) {
widget = (ChartWidget)loadWidget;
} else if (loadWidget instanceof DrillDownWidget) {
Entity entity = ((DrillDownWidget)loadWidget).getEntity();
if (entity instanceof Chart) {
model = new Model<Chart>((Chart)entity);
}
}
} catch (NotFoundException e) {
LOG.error(e.getMessage(), e);
}
}
}
// behavior is called on any refresh, we have to call it only once
// (otherwise the panel will be replaced in the same time the old one is refreshed)
boolean isHTML5 = true;
if (isHTML5String.isEmpty()) {
isHTML5String = param;
isHTML5 = Boolean.parseBoolean(param);
}
final ChartModel chartModel = new ChartModel(model, widget, isHTML5);
// TODO put width, height in settings
if (isHTML5) {
if (zoom) {
ChartHTML5Panel hp = new ChartHTML5Panel("chart", "100%", "100%", chartModel);
//hp.setDetachedPage(true);
container.replace(hp);
} else {
if ((width == null) || (height == null)) {
width = "100%";
height = "300";
}
container.replace(new ChartHTML5Panel("chart", width, height, chartModel));
}
} else {
if (zoom) {
OpenFlashChart ofc = new OpenFlashChart("chart", "100%", "100%", chartModel);
ofc.setDetachedPage(true);
container.replace(ofc);
} else {
if ((width == null) || (height == null)) {
width = "100%";
height = "300";
}
container.replace(new OpenFlashChart("chart", width, height, chartModel));
}
}
target.add(container);
error.replace(new Label("error", new Model<String>()) {
private static final long serialVersionUID = 1L;
@Override
protected void onInitialize() {
super.onInitialize();
if (chartModel.getObject() == null) {
if (chartModel.getError() instanceof NoDataFoundException) {
setDefaultModelObject(getString("ActionContributor.Run.nodata"));
} else {
setDefaultModelObject(ExceptionUtils.getRootCauseMessage(chartModel.getError()));
}
error.add(AttributeAppender.replace("class", "noChartData"));
container.setVisible(false);
}
}
@Override
public boolean isVisible() {
return chartModel.hasError();
}
});
target.add(error);
}
@Override
public boolean getStatelessHint(Component component) {
return false;
}
}
protected void onClickChart(AjaxRequestTarget target, String value, String pattern) throws Exception {
}
}