/* * This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General * Public License, version 2.1 as published by the Free Software Foundation. You should have received a copy of the GNU * Lesser General Public License along with this program; if not, you can obtain a copy at * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html or from the Free Software Foundation, Inc., 51 Franklin * Street, Fifth Floor, Boston, MA 02110-1301 USA. This program 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. Copyright 2008 - 2009 Pentaho Corporation. All rights * reserved. * * Copyright 2009 - Pentaho Corporation * * Written by mbatchelor */ package org.pentaho.platform.plugin.action.chartbeans; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.CONTENT_TYPE; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.INTEGER_TYPE; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.REQUEST_INPUT_SOURCE; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.RESPONSE_OUTPUT_DESTINATION; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.RESULTSET_TYPE; import static org.pentaho.actionsequence.dom.IActionSequenceDocument.STRING_TYPE; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.io.IOUtils; import org.pentaho.actionsequence.dom.ActionSequenceDocument; import org.pentaho.actionsequence.dom.IActionSequenceInput; import org.pentaho.actionsequence.dom.IActionSequenceOutput; import org.pentaho.actionsequence.dom.actions.ActionDefinition; import org.pentaho.actionsequence.dom.actions.MQLAction; import org.pentaho.chart.model.ChartDataDefinition; import org.pentaho.chart.model.ChartModel; import org.pentaho.chart.model.util.ChartSerializer; import org.pentaho.chart.model.util.ChartSerializer.ChartSerializationFormat; import org.pentaho.chart.plugin.jfreechart.JFreeChartPlugin; import org.pentaho.chart.plugin.openflashchart.OpenFlashChartPlugin; import org.pentaho.platform.api.engine.ILogger; import org.pentaho.platform.api.engine.IOutputHandler; import org.pentaho.platform.api.engine.IParameterProvider; import org.pentaho.platform.api.engine.IPentahoSession; import org.pentaho.platform.api.engine.IPentahoUrlFactory; import org.pentaho.platform.api.engine.IRuntimeContext; import org.pentaho.platform.api.engine.ISolutionEngine; import org.pentaho.platform.engine.core.output.SimpleOutputHandler; import org.pentaho.platform.engine.core.solution.SimpleParameterProvider; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.engine.services.runtime.TemplateUtil; import org.pentaho.platform.util.UUIDUtil; import org.pentaho.platform.util.web.SimpleUrlFactory; import com.ibm.icu.text.MessageFormat; public class DefaultChartBeansGenerator implements IChartBeansGenerator { private static final String DEFAULT_HTML_TEMPLATE = "<html><head><title>Command: doChart</title>{0}</head><body>{1}</body></html>"; //$NON-NLS-1$ private static String DEFAULT_flashScriptFragment = "<script type=\"text/javascript\">function {dataFunction}() { return /*JSON*/\"{chartJson}\"/*END_JSON*/;}</script>"; //$NON-NLS-1$ private static String DEFAULT_flashObjectFragment = "<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" " //$NON-NLS-1$ + "codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0\" " //$NON-NLS-1$ + "width=\"{chart-width}\" height=\"{chart-height}\" id=\"ofco{chartId}\" align=\"middle\"> " //$NON-NLS-1$ + "<param name=\"allowScriptAccess\" value=\"sameDomain\" /> " //$NON-NLS-1$ + "<param name=\"wmode\" value=\"opaque\">" //$NON-NLS-1$ + "<param name=\"movie\" value=\"{ofc-url}?get-data={dataFunction}\" /> " //$NON-NLS-1$ + "<param name=\"quality\" value=\"high\" /> " //$NON-NLS-1$ + "<embed src=\"{ofc-url}?get-data={dataFunction}\" wmode=\"opaque\" quality=\"high\" bgcolor=\"#FFFFFF\" " //$NON-NLS-1$ + "width=\"{chart-width}\" height=\"{chart-height}\" id=\"ofce{chartId}\" align=\"middle\" allowScriptAccess=\"sameDomain\" type=\"application/x-shockwave-flash\" " //$NON-NLS-1$ + "pluginspage=\"http://www.macromedia.com/go/getflashplayer\" /></object>"; //$NON-NLS-1$ private String htmlTemplate = DEFAULT_HTML_TEMPLATE; private String flashScriptFragment = DEFAULT_flashScriptFragment; private String flashObjectFragment = DEFAULT_flashObjectFragment; public DefaultChartBeansGenerator() { } /** * The engine that processes the parameters from the specific interface methods and writes a chart to the output * stream or returns an input stream for reading. * * @param pentahoSession * @param serializedChartDataDefinition * @param serializedChartModel * @param chartWidth * @param chartHeight * @param outputStream * @return * @throws IOException */ private InputStream internalCreateChart(IPentahoSession pentahoSession, Map<String, String> parameterMap, String serializedChartDataDefinition, String serializedChartModel, int chartWidth, int chartHeight, OutputStream outputStream) throws IOException { InputStream result = null; ByteArrayOutputStream resultOutputStream = null; OutputStream out = null; // Make code more readable by defining the output result boolean returnInputStream = outputStream == null ? true : false; // If the caller sends a null OutputStream, then we will return an InputStream if (returnInputStream) { resultOutputStream = new ByteArrayOutputStream(); out = new BufferedOutputStream(resultOutputStream); } else { out = outputStream; } // Setup parameters to be passed to the xaction Map<String, Object> params = new HashMap<String, Object>(); params.put("chart-model-json", serializedChartModel); //$NON-NLS-1$ params.put("chart-width", chartWidth); //$NON-NLS-1$ params.put("chart-height", chartHeight); //$NON-NLS-1$ // De-serialize the chartDataDefintion and extract relevant parts ChartDataDefinition chartDataDefinition = ChartSerializer.deSerializeDataDefinition(serializedChartDataDefinition, ChartSerializationFormat.JSON); if (chartDataDefinition.getQuery() != null) { params.put("query", chartDataDefinition.getQuery()); //$NON-NLS-1$ } if (chartDataDefinition.getDomainColumn() != null) { params.put("series-column", chartDataDefinition.getDomainColumn()); //$NON-NLS-1$ } if (chartDataDefinition.getCategoryColumn() != null) { params.put("category-column", chartDataDefinition.getCategoryColumn()); //$NON-NLS-1$ } if (chartDataDefinition.getRangeColumn() != null) { params.put("value-column", chartDataDefinition.getRangeColumn()); //$NON-NLS-1$ } if (chartDataDefinition.getScalingFactor() != null) { params.put("scaling-factor", chartDataDefinition.getScalingFactor().toString()); //$NON-NLS-1$ } createAndRunActionSequence(pentahoSession, params, parameterMap, out); if (out instanceof BufferedOutputStream) { out.flush(); } if (returnInputStream) { result = new ByteArrayInputStream(resultOutputStream.toByteArray()); return (result); } return null; } protected void createAndRunActionSequence(final IPentahoSession pentahoSession, final Map<String, Object> params, final Map<String, String> defaultParameterMap, final OutputStream out) { SimpleParameterProvider parameterProvider = new SimpleParameterProvider(params); // add the default parameter values for (Map.Entry<String, String> entry : defaultParameterMap.entrySet()) { parameterProvider.setParameter(entry.getKey(), entry.getValue()); } Map<String, IParameterProvider> parameterProviders = new HashMap<String, IParameterProvider>(); parameterProviders.put(IParameterProvider.SCOPE_REQUEST, parameterProvider); SimpleOutputHandler outputHandler = new SimpleOutputHandler(out, true); outputHandler.setOutputPreference(IOutputHandler.OUTPUT_TYPE_DEFAULT); ActionSequenceDocument doc = createActionSequenceDocument(defaultParameterMap.keySet()); runActionSequence(pentahoSession, parameterProviders, outputHandler, doc); try { out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } /** * Executes an action sequence from an <code>ActionSequenceDocument</code>. * * @param pentahoSession * current <code>IPentahoSession</code> * @param parameterProviders * map of parameter providers; there should a single entry with "request" as the key * @param outputHandler * output handler * @param doc * action sequence document * @throws RuntimeException * if anything goes wrong */ protected void runActionSequence(final IPentahoSession pentahoSession, final Map<String, IParameterProvider> parameterProviders, final IOutputHandler outputHandler, final ActionSequenceDocument doc) throws RuntimeException { // Get the solution engine ISolutionEngine solutionEngine = PentahoSystem.get(ISolutionEngine.class, pentahoSession); if (solutionEngine == null) { throw new RuntimeException("solutionEngine is null"); //$NON-NLS-1$ } solutionEngine.setLoggingLevel(ILogger.DEBUG); solutionEngine.init(pentahoSession); IPentahoUrlFactory urlFactory = new SimpleUrlFactory(PentahoSystem.getApplicationContext().getBaseUrl()); IRuntimeContext runtime = solutionEngine.execute(doc.toString(), "chartbeans_mql", "myprocessid", false, true, //$NON-NLS-1$ //$NON-NLS-2$ "myinstanceid", true, parameterProviders, outputHandler, null, urlFactory, new ArrayList()); //$NON-NLS-1$ if ((runtime != null) && (runtime.getStatus() == IRuntimeContext.RUNTIME_STATUS_SUCCESS)) { } else { StringBuilder buf = new StringBuilder(); boolean firstIteration = true; for (Object /* String */message : runtime.getMessages()) { if (!firstIteration) { buf.append(" \\\\ "); //$NON-NLS-1$ } buf.append(message); } throw new RuntimeException(buf.toString()); } } /** * Creates an <code>ActionSequenceDocument</code> that will run an MQL query and pipe the results in the ChartBeans * <code>ChartComponent</code>. * * @param parameterNameSet * set of parameter names that appear in the MQL query * @return doc */ protected ActionSequenceDocument createActionSequenceDocument(final Set<String> parameterNameSet) { ActionSequenceDocument actionSequenceDocument = new ActionSequenceDocument(); actionSequenceDocument.setTitle("chartbeans_mql.xaction"); //$NON-NLS-1$ actionSequenceDocument.setVersion("1"); //$NON-NLS-1$ actionSequenceDocument.setLoggingLevel("debug"); //$NON-NLS-1$ actionSequenceDocument.setAuthor("Dashboard"); //$NON-NLS-1$ actionSequenceDocument.setDescription("Generate a chart through ChartBeans from an MQL statement."); //$NON-NLS-1$ actionSequenceDocument.setHelp("Pass in an MQL statement that returns a table of three columns. The first column " //$NON-NLS-1$ + "is the series, the second is the category and the third is the data."); //$NON-NLS-1$ actionSequenceDocument.setHelp(""); //$NON-NLS-1$ actionSequenceDocument.setResultType("rule"); //$NON-NLS-1$ IActionSequenceInput queryInput = actionSequenceDocument.createInput("query", STRING_TYPE); //$NON-NLS-1$ IActionSequenceInput chartModelJsonInput = actionSequenceDocument.createInput("chart-model-json", STRING_TYPE); //$NON-NLS-1$ IActionSequenceInput chartWidthInput = actionSequenceDocument.createInput("chart-width", INTEGER_TYPE); //$NON-NLS-1$ chartWidthInput.addSource(REQUEST_INPUT_SOURCE, "chart-width"); //$NON-NLS-1$ chartWidthInput.setDefaultValue("1"); //$NON-NLS-1$ IActionSequenceInput chartHeightInput = actionSequenceDocument.createInput("chart-height", INTEGER_TYPE); //$NON-NLS-1$ chartHeightInput.addSource(REQUEST_INPUT_SOURCE, "chart-height"); //$NON-NLS-1$ chartHeightInput.setDefaultValue("1"); //$NON-NLS-1$ IActionSequenceInput seriesColumnInput = actionSequenceDocument.createInput("series-column", STRING_TYPE); //$NON-NLS-1$ seriesColumnInput.setDefaultValue("1"); //$NON-NLS-1$ IActionSequenceInput categoryColumnInput = actionSequenceDocument.createInput("category-column", STRING_TYPE); //$NON-NLS-1$ // set a default value of empty string to avoid an error when rendering pie charts (which don't have a category column categoryColumnInput.setDefaultValue("2"); //$NON-NLS-1$ IActionSequenceInput valueColumnInput = actionSequenceDocument.createInput("value-column", STRING_TYPE); //$NON-NLS-1$ valueColumnInput.setDefaultValue("0"); //$NON-NLS-1$ IActionSequenceInput scalingFactorInput = actionSequenceDocument.createInput("scaling-factor", STRING_TYPE); //$NON-NLS-1$ // add inputs from parameterNameSet; these parameters will appear as placeholders in the query input for (String parameterName : parameterNameSet) { actionSequenceDocument.createInput(parameterName, STRING_TYPE); } IActionSequenceOutput outputStreamOutput = actionSequenceDocument.createOutput("outputstream", CONTENT_TYPE); //$NON-NLS-1$ outputStreamOutput.addDestination(RESPONSE_OUTPUT_DESTINATION, "content"); //$NON-NLS-1$ MQLAction mqlAction = (MQLAction) actionSequenceDocument.addAction(MQLAction.class); mqlAction.setActionInputValue("query", queryInput); //$NON-NLS-1$ // add inputs from parameterNameSet to this action for (String parameterName : parameterNameSet) { mqlAction.addInput(parameterName, STRING_TYPE); } mqlAction.setOutputResultSet("chartdata"); //$NON-NLS-1$ mqlAction.setComponentDefinition("live", Boolean.TRUE.toString()); //$NON-NLS-1$ mqlAction.setComponentDefinition("display-names", Boolean.FALSE.toString()); //$NON-NLS-1$ ActionDefinition pojoAction = (ActionDefinition) actionSequenceDocument.addAction(ActionDefinition.class); pojoAction.setComponentName("ChartBeansComponent"); //$NON-NLS-1$ pojoAction.setActionInputValue("chart-model-json", chartModelJsonInput); //$NON-NLS-1$ pojoAction.addInput("chartdata", RESULTSET_TYPE); //$NON-NLS-1$ pojoAction.setActionInputValue("chart-width", chartWidthInput); //$NON-NLS-1$ pojoAction.setActionInputValue("chart-height", chartHeightInput); //$NON-NLS-1$ pojoAction.setActionInputValue("series-column", seriesColumnInput); //$NON-NLS-1$ pojoAction.setActionInputValue("category-column", categoryColumnInput); //$NON-NLS-1$ pojoAction.setActionInputValue("value-column", valueColumnInput); //$NON-NLS-1$ pojoAction.setActionInputValue("scaling-factor", scalingFactorInput); //$NON-NLS-1$ pojoAction.addOutput("outputstream", CONTENT_TYPE); //$NON-NLS-1$ return actionSequenceDocument; } public String buildEmptyOpenFlashChartHtmlFragment(String msg) { // populate the flash html template Properties props = new Properties(); props.setProperty("dataFunction", "getChartData"); // + chartId); //$NON-NLS-1$ //$NON-NLS-2$ props.setProperty("chartJson", "{}"); //$NON-NLS-1$ //$NON-NLS-2$ String flashHtml = MessageFormat.format(getHtmlTemplate(), new String[] { TemplateUtil.applyTemplate(getFlashScriptFragment(), props, null), msg }); return flashHtml; } /** * Does this method belong in ChartBeansGeneratorUtil? ChartBeansGeneratorUtil may be more of a convenience for * executing the default ActionSequence, if this is to hold true, this method probably needs a new home more central * to the ChartBeans code. Returns a complete HTML document that references an Open Flash Chart SWF resource that * resides on the server along with the data that should be displayed in the chart (via a JavaScript function that * returns a JSON string). * <p> * Only exposed for debugging (i.e. hosted mode) purposes. * </p> */ public String buildOpenFlashChartHtmlFragment(String openFlashChartJson, String swfUrl, String chartWidth, String chartHeight) { // generate a unique name for the function String chartId = UUIDUtil.getUUIDAsString().replaceAll("[^\\w]", ""); //$NON-NLS-1$ //$NON-NLS-2$ // populate the flash html template Properties props = new Properties(); props.setProperty("chartId", chartId); //$NON-NLS-1$ props.setProperty("dataFunction", "getChartData"); // + chartId); //$NON-NLS-1$ //$NON-NLS-2$ props.setProperty("chart-width", chartWidth); //$NON-NLS-1$ props.setProperty("chart-height", chartHeight); //$NON-NLS-1$ props.setProperty("ofc-url", swfUrl); //$NON-NLS-1$ props.setProperty("chartJson", openFlashChartJson); //$NON-NLS-1$ String flashHtml = MessageFormat.format(getHtmlTemplate(), new String[] { TemplateUtil.applyTemplate(getFlashScriptFragment(), props, null), TemplateUtil.applyTemplate(getFlashObjectFragment(), props, null) }); return flashHtml; } public String createChartAsHtml(IPentahoSession userSession, Map<String, String> parameterMap, String serializedChartDataDefinition, String serializedChartModel, int chartWidth, int chartHeight) throws IOException { ChartModel chartModel = ChartSerializer.deSerialize(serializedChartModel, ChartSerializationFormat.JSON); String html = null; if(chartModel.getChartEngineId() == null){ // Load default value from system setting or take hard coded // Hard coded final fall back is Open Flash Chart String defaultChartEngine = PentahoSystem.getSystemSetting("chartbeans/chartbeans_config.xml", "default-chart-engine", OpenFlashChartPlugin.PLUGIN_ID);//$NON-NLS-1$ //$NON-NLS-2$ if(defaultChartEngine == null){ defaultChartEngine = OpenFlashChartPlugin.PLUGIN_ID; } chartModel.setChartEngineId(defaultChartEngine); } if (JFreeChartPlugin.PLUGIN_ID.equals(chartModel.getChartEngineId())) { final String SOLUTION_TMP_DIR = "system/tmp/"; //$NON-NLS-1$ File chartFileOnServer = new File(new File(PentahoSystem.getApplicationContext().getFileOutputPath( SOLUTION_TMP_DIR)), java.util.UUID.randomUUID().toString()); BufferedOutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(chartFileOnServer)); this.internalCreateChart(userSession, parameterMap, serializedChartDataDefinition, serializedChartModel, chartWidth, chartHeight, bos); } finally { IOUtils.closeQuietly(bos); } final String IMAGE_URL_TEMPLATE = "{0}getImage?image={1}"; //$NON-NLS-1$ final String imageUrl = MessageFormat.format(IMAGE_URL_TEMPLATE, new String[] { PentahoSystem.getApplicationContext().getBaseUrl(), chartFileOnServer.getName() }); html = this.mergeStaticImageHtmlTemplate(imageUrl); } else if (OpenFlashChartPlugin.PLUGIN_ID.equals(chartModel.getChartEngineId())) { ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(); this.internalCreateChart(userSession, parameterMap, serializedChartDataDefinition, serializedChartModel, chartWidth, chartHeight, tmpOut); final String ENCODING = "UTF-8"; //$NON-NLS-1$ ByteArrayInputStream in = new ByteArrayInputStream(tmpOut.toByteArray()); IOUtils.closeQuietly(tmpOut); html = IOUtils.toString(in, ENCODING); IOUtils.closeQuietly(in); } else { throw new IllegalArgumentException("unrecognized chart engine"); //$NON-NLS-1$ } return html; } public String mergeOpenFlashChartHtmlTemplate(String openFlashChartJson, String swfUrl) { return buildOpenFlashChartHtmlFragment(openFlashChartJson, swfUrl, "100%", "100%"); //$NON-NLS-1$ //$NON-NLS-2$ } public String mergeStaticImageHtmlTemplate(String imageUrl) { final String BODY_TEMPLATE = "<img src=\"{0}\" />"; //$NON-NLS-1$ final String body = MessageFormat.format(BODY_TEMPLATE, new String[] { imageUrl }); return MessageFormat.format(getHtmlTemplate(), new String[] { "", body }); //$NON-NLS-1$ } public String getHtmlTemplate() { return this.htmlTemplate; } public String getFlashScriptFragment() { return this.flashScriptFragment; } public String getFlashObjectFragment() { return this.flashObjectFragment; } }