/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* 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.xwiki.rendering.internal.macro.chart;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.chart.ChartGenerator;
import org.xwiki.chart.ChartGeneratorException;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.ImageBlock;
import org.xwiki.rendering.block.LinkBlock;
import org.xwiki.rendering.block.ParagraphBlock;
import org.xwiki.rendering.internal.macro.chart.source.DataSource;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.listener.reference.ResourceType;
import org.xwiki.rendering.macro.AbstractMacro;
import org.xwiki.rendering.macro.MacroExecutionException;
import org.xwiki.rendering.macro.chart.ChartMacroParameters;
import org.xwiki.rendering.macro.descriptor.DefaultContentDescriptor;
import org.xwiki.rendering.transformation.MacroTransformationContext;
/**
* A macro for rendering charts.
*
* @version $Id: ceca8b8041508d0dee9cafb2526524e7a2262fc6 $
* @since 2.0M1
*/
@Component
@Named("chart")
@Singleton
public class ChartMacro extends AbstractMacro<ChartMacroParameters>
{
/**
* The description of the macro.
*/
private static final String DESCRIPTION = "Displays a graphical chart generated from miscellaneous data sources";
/**
* The description of the macro content.
*/
private static final String CONTENT_DESCRIPTION = "Input data for the chart macro (Ex. for 'inline' source mode)";
/**
* Used for building the actual chart.
*/
@Inject
private ChartGenerator chartGenerator;
/**
* The component manager needed for instantiating the datasource factory.
*/
@Inject
private ComponentManager componentManager;
/**
* Used to compute the chart image storage location and URL to access it.
*/
@Inject
@Named("tmp")
private ChartImageWriter imageWriter;
/**
* Create and initialize the descriptor of the macro.
*/
public ChartMacro()
{
super("Chart", DESCRIPTION, new DefaultContentDescriptor(CONTENT_DESCRIPTION, false),
ChartMacroParameters.class);
setDefaultCategory(DEFAULT_CATEGORY_CONTENT);
}
@Override
public boolean supportsInlineMode()
{
return true;
}
@Override
public List<Block> execute(ChartMacroParameters macroParams, String content, MacroTransformationContext context)
throws MacroExecutionException
{
// Generate the chart image in a temporary location.
generateChart(macroParams, content, context);
String imageLocation = this.imageWriter.getURL(new ImageId(macroParams));
String title = macroParams.getTitle();
ResourceReference reference = new ResourceReference(imageLocation, ResourceType.URL);
ImageBlock imageBlock = new ImageBlock(new ResourceReference(imageLocation, ResourceType.URL), true);
imageBlock.setParameter("alt", title);
LinkBlock linkBlock = new LinkBlock(Collections.singletonList((Block) imageBlock), reference, true);
linkBlock.setParameter("title", title);
// If the macro is used standalone then we need to wrap it in a paragraph block.
Block resultBlock;
if (context.isInline()) {
resultBlock = linkBlock;
} else {
resultBlock = new ParagraphBlock(Collections.singletonList((Block) linkBlock));
}
return Collections.singletonList(resultBlock);
}
/**
* Builds the chart image according to the specifications passed in.
*
* @param parameters the macro parameters
* @param content the macro content
* @param context the macro transformation context, used for example to find out the current document reference
* @throws MacroExecutionException if an error occurs while generating / saving the chart image
*/
private void generateChart(ChartMacroParameters parameters, String content, MacroTransformationContext context)
throws MacroExecutionException
{
String source = computeSource(parameters.getSource(), content);
DataSource dataSource;
try {
dataSource = this.componentManager.getInstance(DataSource.class, source);
} catch (ComponentLookupException e) {
throw new MacroExecutionException(String.format("Invalid source parameter [%s]",
parameters.getSource()), e);
}
Map<String, String> sourceParameters = getSourceParameters(parameters, source);
dataSource.buildDataset(content, sourceParameters, context);
try {
this.imageWriter.writeImage(new ImageId(parameters),
this.chartGenerator.generate(dataSource.getChartModel(), sourceParameters));
} catch (ChartGeneratorException e) {
throw new MacroExecutionException("Error while rendering chart", e);
}
}
/**
* Compute what Data Source to use. If the user has specified one then use it. Otherwise if there's content
* in the macro default to using the "inline" source and if not default to using the "xdom" source.
*
* @param userDefinedSource the user specified source value from the Macro parameter (is null if not specified)
* @param content the Macro content
* @return the hint of the {@link DataSource} component to use
*/
private String computeSource(String userDefinedSource, String content)
{
String source = userDefinedSource;
if (source == null) {
if (StringUtils.isEmpty(content)) {
source = "xdom";
} else {
source = "inline";
}
}
return source;
}
/**
* TODO There is no way to escape the ';' character.
*
* @param chartMacroParameters The macro parameters.
* @param sourceHint the hint of the Data Source component to use
* @return A map containing the source parameters.
*/
private Map<String, String> getSourceParameters(ChartMacroParameters chartMacroParameters, String sourceHint)
{
Map<String, String> parameters = new HashMap<String, String>();
parameters.put(ChartGenerator.TITLE_PARAM, chartMacroParameters.getTitle());
parameters.put(ChartGenerator.WIDTH_PARAM, String.valueOf(chartMacroParameters.getWidth()));
parameters.put(ChartGenerator.HEIGHT_PARAM, String.valueOf(chartMacroParameters.getHeight()));
parameters.put(ChartGenerator.TYPE_PARAM, chartMacroParameters.getType());
parameters.put(DataSource.SOURCE_PARAM, sourceHint);
parameters.put(DataSource.PARAMS_PARAM, chartMacroParameters.getParams());
String sourceParameters = chartMacroParameters.getParams();
if (null != sourceParameters) {
String[] segments = sourceParameters.split(";");
for (String segment : segments) {
String[] keyValue = segment.split(":", 2);
String key = StringUtils.trim(keyValue[0]);
if (keyValue.length == 2) {
parameters.put(key, keyValue[1]);
} else {
parameters.put(key, null);
}
}
}
return parameters;
}
}