/* * JBoss, Home of Professional Open Source * Copyright 2013, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This 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 2.1 of * the License, or (at your option) any later version. * * This software 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 this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.richfaces.renderkit; import static java.util.Arrays.asList; import static org.richfaces.renderkit.RenderKitUtils.addToScriptHash; import static org.richfaces.renderkit.RenderKitUtils.attributes; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.el.MethodExpression; import javax.faces.FacesException; import javax.faces.component.UIComponent; import javax.faces.component.visit.VisitCallback; import javax.faces.component.visit.VisitContext; import javax.faces.component.visit.VisitResult; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import org.richfaces.component.AbstractChart; import org.richfaces.component.AbstractChartLegend; import org.richfaces.component.AbstractChartPoint; import org.richfaces.component.AbstractChartSeries; import org.richfaces.component.AbstractChartXAxis; import org.richfaces.component.AbstractChartYAxis; import org.richfaces.json.JSONArray; import org.richfaces.json.JSONException; import org.richfaces.json.JSONObject; import org.richfaces.model.ChartDataModel; import org.richfaces.model.ChartDataModel.ChartType; import org.richfaces.model.NumberChartDataModel; import org.richfaces.model.PlotClickEvent; import org.richfaces.model.RawJSONString; import org.richfaces.model.StringChartDataModel; /** * @author Lukas Macko */ public abstract class ChartRendererBase extends RendererBase { public static final String RENDERER_TYPE = "org.richfaces.ChartRenderer"; private static final String X_VALUE = "x"; private static final String Y_VALUE = "y"; private static final String POINT_INDEX = "dataIndex"; private static final String SERIES_INDEX = "seriesIndex"; private static final String EVENT_TYPE = "name"; private static final String PLOT_CLICK_TYPE = "plotclick"; /** * Method adds key-value pair to object. * @param obj * @param key * @param value * @return * @throws IOException * if put to JSONObject fails */ public static JSONObject addAttribute(JSONObject obj, String key, Object value) throws IOException { try { if (value != null && !value.equals("")) { obj.put(key, value); } } catch (JSONException ex) { throw new IOException("JSONObject put failed."); } return obj; } /** * Method creates JSON containing chart options * @param context * @param component * @return * @throws IOException */ public JSONObject getOpts(FacesContext context, UIComponent component) throws IOException { JSONObject obj = new JSONObject(); addAttribute(obj, "zoom", component.getAttributes().get("zoom")); addAttribute(obj, "charttype", component.getAttributes().get("charttype")); addAttribute(obj, "xtype", component.getAttributes().get("xtype")); addAttribute(obj, "ytype", component.getAttributes().get("ytype")); addAttribute(obj, "serverSideListener", component.getAttributes().get("serverSideListener")); JSONObject xaxis = new JSONObject(); addAttribute(xaxis, "min", component.getAttributes().get("xmin")); addAttribute(xaxis, "max", component.getAttributes().get("xmax")); addAttribute(xaxis, "autoscaleMargin", component.getAttributes().get("xpad")); addAttribute(xaxis, "axisLabel", component.getAttributes() .get("xlabel")); addAttribute(xaxis, "format", component.getAttributes().get("xformat")); JSONObject yaxis = new JSONObject(); addAttribute(yaxis, "min", component.getAttributes().get("ymin")); addAttribute(yaxis, "max", component.getAttributes().get("ymax")); addAttribute(yaxis, "autoscaleMargin", component.getAttributes().get("ypad")); addAttribute(yaxis, "axisLabel", component.getAttributes() .get("ylabel")); addAttribute(yaxis, "format", component.getAttributes().get("yformat")); JSONObject legend = new JSONObject(); addAttribute(legend, "position", component.getAttributes().get("position")); addAttribute(legend, "sorted", component.getAttributes().get("sorting")); addAttribute(obj, "xaxis", xaxis); addAttribute(obj, "yaxis", yaxis); addAttribute(obj, "legend", legend); return obj; } @Override public void doDecode(FacesContext context, UIComponent component) { if (!component.isRendered()) { return; } Map<String, String> requestParameterMap = context.getExternalContext() .getRequestParameterMap(); if (requestParameterMap.get(component.getClientId(context)) != null) { String xParam = requestParameterMap.get(getFieldId(component, X_VALUE)); String yParam = requestParameterMap.get(getFieldId(component, Y_VALUE)); String pointIndexParam = requestParameterMap.get(getFieldId( component, POINT_INDEX)); String eventTypeParam = requestParameterMap.get(getFieldId( component, EVENT_TYPE)); String seriesIndexParam = requestParameterMap.get(getFieldId( component, SERIES_INDEX)); try { if (PLOT_CLICK_TYPE.equals(eventTypeParam)) { double y = Double.parseDouble(yParam); String x = xParam; if (seriesIndexParam == null) { new PlotClickEvent(component, -1, -1, x, y).queue(); return; } int seriesIndex = Integer.parseInt(seriesIndexParam); int pointIndex = Integer.parseInt(pointIndexParam); new PlotClickEvent(component, seriesIndex, pointIndex, x, y) .queue(); } } catch (NumberFormatException ex) { throw new FacesException("Cannot convert request parmeters", ex); } } } /** * Returns chart chart data * * @param ctx * @param component * @return */ public JSONArray getChartData(FacesContext ctx, UIComponent component) { return (JSONArray) component.getAttributes().get("chartData"); } /** * Method process chart tags, it collects chart options and data. */ @Override public void doEncodeBegin(ResponseWriter writer, FacesContext context, UIComponent component) throws IOException { AbstractChart chart = (AbstractChart) component; VisitChart visitCallback = new VisitChart(chart); // copy attributes to parent tag and process data chart.visitTree(VisitContext.createVisitContext(FacesContext .getCurrentInstance()), visitCallback); // store data to parent tag component.getAttributes().put("chartData", visitCallback.getData()); if (!visitCallback.isDataEmpty()) { component.getAttributes().put("charttype", visitCallback.getChartType()); component.getAttributes().put("xtype", axisDataTypeToString(visitCallback.getKeyType())); component.getAttributes().put("ytype", axisDataTypeToString(visitCallback.getValType())); } //set flag whether request to server should be sent boolean anyServerSideListener = chart.getPlotClickListener()!=null?true:false; if(!anyServerSideListener){ //check if there is particular series listener List<MethodExpression> listeners = visitCallback.getParticularSeriesListeners(); for (MethodExpression methodExpression : listeners) { if(methodExpression!=null){ anyServerSideListener=true; break; } } } component.getAttributes().put("serverSideListener",anyServerSideListener); //client-side handlers for particular series component.getAttributes().put("handlers", visitCallback.getSeriesSpecificHandlers()); //server-side listeners for particular series component.getAttributes().put("particularSeriesListeners", visitCallback.getParticularSeriesListeners()); } /** * Converts class name of data type used in axes to shorter string * representation ie. class java.lang.String -> string * * @param c * @return */ public String axisDataTypeToString(Class c) { if (c == String.class) { return "string"; } else if (c == Number.class) { return "number"; } else if (c == Date.class) { return "date"; } else { return c.getName(); } } public JSONObject getParticularSeriesHandler(FacesContext context, UIComponent component) { return (JSONObject) component.getAttributes().get("handlers"); }; /** * Method creates unique identifier for request parameter. * @param component * @param attribute * @return */ public String getFieldId(UIComponent component, String attribute) { return component.getClientId() + attribute; } /** * Callback loop through children tags: axis, series, legend */ class VisitChart implements VisitCallback { private final AbstractChart chart; private final JSONArray data; private final JSONObject particularSeriesHandlers; private final JSONArray plotClickHandlers; private final JSONArray plothoverHandlers; private final List<MethodExpression> particularSeriesListeners; private ChartDataModel.ChartType chartType; private Class keyType; private Class valType; private final RenderKitUtils.ScriptHashVariableWrapper eventWrapper = RenderKitUtils.ScriptHashVariableWrapper.eventHandler; private boolean nodata; public VisitChart(AbstractChart ch) { this.nodata = true; this.chart = ch; this.chartType = null; this.data = new JSONArray(); this.particularSeriesHandlers = new JSONObject(); this.plotClickHandlers = new JSONArray(); this.plothoverHandlers = new JSONArray(); this.particularSeriesListeners = new LinkedList<MethodExpression>(); try { addAttribute(particularSeriesHandlers, "onplotclick", plotClickHandlers); addAttribute(particularSeriesHandlers, "onplothover", plothoverHandlers); } catch (IOException ex) { throw new FacesException(ex); } } private void copyAttr(UIComponent src, UIComponent target, String prefix, String attr) { Object val = src.getAttributes().get(attr); if (val != null) { target.getAttributes().put(prefix + attr, val); } } /** * Copy attributes from source UIComponent to target. * @param src * @param target * @param prefix * @param attrs */ private void copyAttrs(UIComponent src, UIComponent target, String prefix, List<String> attrs) { for (Iterator<String> it = attrs.iterator(); it.hasNext();) { String attr = it.next(); copyAttr(src, target, prefix, attr); } } @Override public VisitResult visit(VisitContext context, UIComponent target) { if (target instanceof AbstractChartLegend) { copyAttrs(target, chart, "", asList("position", "sorting")); } else if (target instanceof AbstractChartSeries) { AbstractChartSeries s = (AbstractChartSeries) target; ChartDataModel model = s.getData(); particularSeriesListeners.add(s.getPlotClickListener()); // Collect Series specific handlers Map<String, Object> optMap = new HashMap<String, Object>(); RenderKitUtils.Attributes seriesEvents = attributes().generic( "onplothover", "onplothover", "plothover").generic( "onplotclick", "onplotclick", "plotclick"); addToScriptHash(optMap, context.getFacesContext(), target, seriesEvents, RenderKitUtils.ScriptHashVariableWrapper.eventHandler); if (optMap.get("onplotclick") != null) { plotClickHandlers.put(new RawJSONString(optMap.get( "onplotclick").toString())); } else { plotClickHandlers.put(s.getOnplotclick()); } if (optMap.get("onplothover") != null) { plothoverHandlers.put(new RawJSONString(optMap.get( "onplothover").toString())); } else { plothoverHandlers.put(s.getOnplothover()); } // end collect series specific handler if (model == null) { /** * data model priority: if there is data model passed * through data attribute use it. Otherwise nested point * tags are expected. */ VisitSeries seriesCallback = new VisitSeries(s.getType()); s.visitTree(VisitContext.createVisitContext(FacesContext .getCurrentInstance()), seriesCallback); model = seriesCallback.getModel(); // if series has no data create empty model if (model == null) { switch (s.getType()) { case line: model = new NumberChartDataModel(ChartType.line); break; case bar: model = new NumberChartDataModel(ChartType.bar); break; case pie: model = new StringChartDataModel(ChartType.pie); break; default: break; } } else { nodata = false; } } else { nodata = false; } model.setAttributes(s.getAttributes()); try { // Check model/series compatibility if (chartType == null && (!nodata)) { // if series is empty do not set types chartType = model.getType(); keyType = model.getKeyType(); valType = model.getValueType(); } else { if (chartType == ChartDataModel.ChartType.pie) { throw new IllegalArgumentException( "Pie chart supports only one series."); } } if (keyType != model.getKeyType() || valType != model.getValueType()) { throw new IllegalArgumentException( "Data model is not valid for this chart type."); } data.put(model.export()); } catch (IOException ex) { throw new FacesException(ex); } } else if (target instanceof AbstractChartXAxis) { copyAttrs(target, chart, "x", asList("min", "max", "pad", "label", "format")); } else if (target instanceof AbstractChartYAxis) { copyAttrs(target, chart, "y", asList("min", "max", "pad", "label", "format")); } return VisitResult.ACCEPT; } public boolean isDataEmpty() { return nodata; } public JSONArray getData() { return data; } public Class getKeyType() { return keyType; } public Class getValType() { return valType; } public ChartDataModel.ChartType getChartType() { return chartType; } public JSONObject getSeriesSpecificHandlers() { return particularSeriesHandlers; } public List<MethodExpression> getParticularSeriesListeners() { return particularSeriesListeners; } } /** * Callback loops through series children tags - points */ class VisitSeries implements VisitCallback { private ChartDataModel model = null; private final ChartDataModel.ChartType type; public VisitSeries(ChartDataModel.ChartType type) { this.type = type; } @Override public VisitResult visit(VisitContext context, UIComponent target) { if (target instanceof AbstractChartPoint) { AbstractChartPoint p = (AbstractChartPoint) target; Object x = p.getX(); Object y = p.getY(); // the first point determine type of data model if (model == null) { if (x instanceof Number && y instanceof Number) { model = new NumberChartDataModel(type); } else if (x instanceof String && y instanceof Number) { model = new StringChartDataModel(type); } else { throw new IllegalArgumentException("Not supported type"); } } if (model.getKeyType().isAssignableFrom(x.getClass()) && model.getValueType().isAssignableFrom(y.getClass())) { if (x instanceof Number && y instanceof Number) { model.put(x, y); } else if (x instanceof String && y instanceof Number) { model.put(x, y); } else { throw new IllegalArgumentException( "Not supported types " + x.getClass() + " " + y.getClass() + " for " + model.getClass()); } } else { throw new IllegalArgumentException("Not supported types " + x.getClass() + " " + y.getClass() + " for " + model.getClass()); } } return VisitResult.ACCEPT; } public ChartDataModel getModel() { return model; } } }