package org.mapfish.print.processor.jasper;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.collect.Lists;
import jsr166y.ForkJoinPool;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.data.JRTableModelDataSource;
import org.mapfish.print.Constants;
import org.mapfish.print.attribute.LegendAttribute.LegendAttributeValue;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.Template;
import org.mapfish.print.http.MfClientHttpRequestFactory;
import org.mapfish.print.processor.AbstractProcessor;
import org.mapfish.print.processor.http.MfClientHttpRequestFactoryProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
/**
* <p>Create a legend.</p>
* <p>See also: <a href="attributes.html#!legend">!legend</a> attribute</p>
* [[examples=verboseExample,legend_cropped]]
*/
public final class LegendProcessor extends AbstractProcessor<LegendProcessor.Input, LegendProcessor.Output> {
private static final Logger LOGGER = LoggerFactory.getLogger(LegendProcessor.class);
private static final String NAME_COLUMN = "name";
private static final String ICON_COLUMN = "icon";
private static final String REPORT_COLUMN = "report";
private static final String LEVEL_COLUMN = "level";
@Autowired
private JasperReportBuilder jasperReportBuilder;
@Autowired
private MetricRegistry metricRegistry;
@Resource(name = "requestForkJoinPool")
private ForkJoinPool requestForkJoinPool;
private Dimension missingImageSize = new Dimension(24, 24);
private BufferedImage missingImage;
private Color missingImageColor = Color.PINK;
private String template;
private Integer maxWidth = null;
private Double dpi = Constants.PDF_DPI;
/**
* Constructor.
*/
protected LegendProcessor() {
super(Output.class);
}
/**
* The path to the Jasper Report template for rendering the legend data.
*
* @param template path to the template file
*/
public void setTemplate(final String template) {
this.template = template;
}
/**
* The maximum width in pixels for the legend graphics.
* If this parameter is set, the legend graphics are cropped to the given maximum
* width. In this case a sub-report is created containing the graphic.
* For reference see the example [[examples=legend_cropped]].
*
* @param maxWidth The max. width.
*/
public void setMaxWidth(final Integer maxWidth) {
this.maxWidth = maxWidth;
}
/**
* The DPI value that is used for the legend graphics.
* Note: This parameter is only considered when `maxWidth` is set.
*
* @param dpi The DPI value.
*/
public void setDpi(final Double dpi) {
this.dpi = dpi;
}
@Override
public Input createInputParameter() {
return new Input();
}
@Override
public Output execute(final Input values, final ExecutionContext context) throws Exception {
final List<Object[]> legendList = new ArrayList<Object[]>();
final String[] legendColumns = {NAME_COLUMN, ICON_COLUMN, REPORT_COLUMN, LEVEL_COLUMN};
final LegendAttributeValue legendAttributes = values.legend;
fillLegend(
values.clientHttpRequestFactoryProvider.get(),
legendAttributes, legendList, context,
values.tempTaskDirectory);
final Object[][] legend = new Object[legendList.size()][];
final JRTableModelDataSource dataSource = new JRTableModelDataSource(new TableDataSource(legendColumns,
legendList.toArray(legend)));
String compiledTemplatePath = compileTemplate(values.template.getConfiguration());
return new Output(dataSource, legendList.size(), compiledTemplatePath);
}
private String compileTemplate(final Configuration configuration) throws JRException {
if (this.template != null) {
final File file = new File(configuration.getDirectory(), this.template);
return this.jasperReportBuilder.compileJasperReport(configuration, file).getAbsolutePath();
}
return null;
}
private class IconTask implements Callable<Object[]> {
private URL icon;
private ExecutionContext context;
private MfClientHttpRequestFactory clientHttpRequestFactory;
private int level;
private File tempTaskDirectory;
public IconTask(final URL icon, final ExecutionContext context,
final int level, final File tempTaskDirectory,
final MfClientHttpRequestFactory clientHttpRequestFactory) {
this.icon = icon;
this.context = context;
this.level = level;
this.clientHttpRequestFactory = clientHttpRequestFactory;
this.tempTaskDirectory = tempTaskDirectory;
}
@Override
public Object[] call() throws IOException, URISyntaxException, JRException {
BufferedImage image = null;
final URI uri = this.icon.toURI();
final String metricName = LegendProcessor.class.getName() + ".read." + uri.getHost();
try {
checkCancelState(this.context);
final ClientHttpRequest request = this.clientHttpRequestFactory.createRequest(uri, HttpMethod.GET);
final Timer.Context timer = LegendProcessor.this.metricRegistry.timer(metricName).time();
final ClientHttpResponse httpResponse = request.execute();
try {
if (httpResponse.getStatusCode() == HttpStatus.OK) {
image = ImageIO.read(httpResponse.getBody());
if (image == null) {
LOGGER.warn("The URL: " + this.icon + " is NOT an image format that can be decoded");
} else {
timer.stop();
}
} else {
LOGGER.warn("Failed to load image from: " + this.icon
+ " due to server side error.\n\tResponse Code: " + httpResponse.getStatusCode()
+ "\n\tResponse Text: " + httpResponse.getStatusText());
}
} finally {
httpResponse.close();
}
} catch (Exception e) {
LOGGER.warn("Failed to load image from: " + this.icon, e);
}
if (image == null) {
image = getMissingImage();
LegendProcessor.this.metricRegistry.counter(metricName + ".error").inc();
}
String report = null;
if (LegendProcessor.this.maxWidth != null) {
// if a max width is given, create a sub-report containing the cropped graphic
report = createSubReport(image, this.tempTaskDirectory).toString();
}
return new Object[] {null, image, report, this.level};
}
}
private class NameTask implements Callable<Object[]> {
private String name;
private int level;
public NameTask(final String name, final int level) {
this.name = name;
this.level = level;
}
@Override
public Object[] call() {
return new Object[]{this.name, null, null, this.level};
}
}
private void createTasks(final MfClientHttpRequestFactory clientHttpRequestFactory,
final LegendAttributeValue legendAttributes,
final ExecutionContext context, final File tempTaskDirectory,
final int level, final List<Callable<Object[]>> tasks) {
int insertNameIndex = tasks.size();
final URL[] icons = legendAttributes.icons;
if (icons != null && icons.length > 0) {
for (URL icon : icons) {
tasks.add(new IconTask(icon, context, level, tempTaskDirectory, clientHttpRequestFactory));
}
}
if (legendAttributes.classes != null) {
for (LegendAttributeValue value : legendAttributes.classes) {
createTasks(clientHttpRequestFactory, value, context, tempTaskDirectory, level + 1, tasks);
}
}
if (!tasks.isEmpty()) {
tasks.add(insertNameIndex, new NameTask(legendAttributes.name, level));
}
}
private void fillLegend(final MfClientHttpRequestFactory clientHttpRequestFactory,
final LegendAttributeValue legendAttributes,
final List<Object[]> legendList,
final ExecutionContext context,
final File tempTaskDirectory) throws ExecutionException, JRException, InterruptedException, IOException {
List<Callable<Object[]>> tasks = new ArrayList<Callable<Object[]>>();
createTasks(clientHttpRequestFactory, legendAttributes, context, tempTaskDirectory, 0, tasks);
List<Future<Object[]>> futures = this.requestForkJoinPool.invokeAll(tasks);
for (Future<Object[]> future : futures) {
legendList.add(future.get());
}
}
private URI createSubReport(final BufferedImage originalImage,
final File tempTaskDirectory) throws IOException, JRException {
assert (this.maxWidth != null);
double scaleFactor = getScaleFactor();
BufferedImage image = originalImage;
if (image.getWidth() * scaleFactor > this.maxWidth) {
image = cropToMaxWidth(image, scaleFactor);
}
URI imageFile = writeToFile(image, tempTaskDirectory);
final ImagesSubReport subReport = new ImagesSubReport(
Lists.newArrayList(imageFile),
new Dimension((int) (image.getWidth() * scaleFactor), (int) (image.getHeight() * scaleFactor)),
this.dpi);
final File compiledReport = File.createTempFile("legend-report-",
JasperReportBuilder.JASPER_REPORT_COMPILED_FILE_EXT, tempTaskDirectory);
subReport.compile(compiledReport);
return compiledReport.toURI();
}
private BufferedImage cropToMaxWidth(final BufferedImage image, final double scaleFactor) {
int width = (int) Math.round(this.maxWidth / scaleFactor);
return image.getSubimage(0, 0, width, image.getHeight());
}
private double getScaleFactor() {
return Constants.PDF_DPI / this.dpi;
}
private URI writeToFile(final BufferedImage image, final File tempTaskDirectory) throws IOException {
File path = File.createTempFile("legend-", ".png", tempTaskDirectory);
ImageIO.write(image, "png", path);
return path.toURI();
}
@Override
protected void extraValidation(final List<Throwable> validationErrors, final Configuration configuration) {
// no checks needed
}
private synchronized BufferedImage getMissingImage() {
if (this.missingImage == null) {
this.missingImage = new BufferedImage(this.missingImageSize.width, this.missingImageSize.height, BufferedImage.TYPE_INT_RGB);
final Graphics2D graphics = this.missingImage.createGraphics();
try {
graphics.setBackground(this.missingImageColor);
graphics.clearRect(0, 0, this.missingImageSize.width, this.missingImageSize.height);
} finally {
graphics.dispose();
}
}
return this.missingImage;
}
/**
* The Input Parameter object for {@link org.mapfish.print.processor.jasper.LegendProcessor}.
*/
public static final class Input {
/**
* The template that contains this processor.
*/
public Template template;
/**
* A factory for making http requests. This is added to the values by the framework and therefore
* does not need to be set in configuration
*/
public MfClientHttpRequestFactoryProvider clientHttpRequestFactoryProvider;
/**
* The path to the temporary directory for the print task.
*/
public File tempTaskDirectory;
/**
* The data required for creating the legend.
*/
public LegendAttributeValue legend;
}
/**
* The Output object of the legend processor method.
*/
public static final class Output {
/**
* The datasource for the legend object in the report.
*/
public final JRTableModelDataSource legendDataSource;
/**
* The path to the compiled subreport.
*/
public final String legendSubReport;
/**
* The number of rows in the legend.
*/
public final int numberOfLegendRows;
Output(final JRTableModelDataSource legendDataSource, final int numberOfLegendRows, final String legendSubReport) {
this.legendDataSource = legendDataSource;
this.numberOfLegendRows = numberOfLegendRows;
this.legendSubReport = legendSubReport;
}
}
}