/**
* @license Highcharts JS v2.3.3 (2012-11-02)
*
* (c) 20012-2014
*
* Author: Gert Vaartjes
*
* License: www.highcharts.com/license
*/
package com.highcharts.export.controller;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.highcharts.export.converter.SVGConverter;
import com.highcharts.export.converter.SVGConverterException;
import com.highcharts.export.pool.PoolException;
import com.highcharts.export.util.MimeType;
import com.highcharts.export.util.TempDir;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RequestBody;
@Controller
@RequestMapping("/")
public class ExportController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Float MAX_WIDTH = 2000.0F;
private static final Float MAX_SCALE = 4.0F;
protected static Logger logger = Logger.getLogger("exporter");
@Autowired
private SVGConverter converter;
@RequestMapping(value = "/demo", method = RequestMethod.GET)
public String demo() {
return "demo";
}
@RequestMapping(method = {RequestMethod.POST, RequestMethod.GET})
public HttpEntity<byte[]> exporter(
@RequestParam(value = "svg", required = false) String svg,
@RequestParam(value = "type", required = false) String type,
@RequestParam(value = "filename", required = false) String filename,
@RequestParam(value = "width", required = false) String width,
@RequestParam(value = "scale", required = false) String scale,
@RequestParam(value = "options", required = false) String options,
@RequestParam(value = "globaloptions", required = false) String globalOptions,
@RequestParam(value = "constr", required = false) String constructor,
@RequestParam(value = "callback", required = false) String callback,
@RequestParam(value = "callbackHC", required = false) String callbackHC,
@RequestParam(value = "async", required = false, defaultValue = "false") Boolean async,
@RequestParam(value = "jsonp", required = false, defaultValue = "false") Boolean jsonp,
HttpServletRequest request,
HttpSession session) throws ServletException, InterruptedException, SVGConverterException, NoSuchElementException, PoolException, TimeoutException, IOException, ZeroRequestParameterException {
MimeType mime = getMime(type);
String randomFilename = null;
String jsonpCallback = "";
boolean isAndroid = request.getHeader("user-agent") != null && request.getHeader("user-agent").contains("Android");
if ("GET".equalsIgnoreCase(request.getMethod())) {
// Handle redirect downloads for Android devices, these come in without request parameters
String tempFile = (String) session.getAttribute("tempFile");
session.removeAttribute("tempFile");
if (tempFile != null && !tempFile.isEmpty()) {
logger.debug("filename stored in session, read and stream from filesystem");
String basename = FilenameUtils.getBaseName(tempFile);
String extension = FilenameUtils.getExtension(tempFile);
return getFile(basename, extension);
}
}
// check for visitors who don't know this domain is really only for the exporting service ;)
if (request.getParameterMap().isEmpty()) {
throw new ZeroRequestParameterException();
}
/* Most JSONP implementations use the 'callback' request parameter and this overwrites
* the original callback parameter for chart creation with Highcharts. If JSONP is
* used we recommend using the requestparameter callbackHC as the callback for Highcharts.
* store the callback method name and reset the callback parameter,
* otherwise it will be used when creation charts
*/
if (jsonp) {
async = true;
jsonpCallback = callback;
callback = null;
if (callbackHC != null) {
callback = callbackHC;
}
}
if (isAndroid || MimeType.PDF.equals(mime) || async) {
randomFilename = createRandomFileName(mime.name().toLowerCase());
}
/* If randomFilename is not null, then we want to save the filename in session, in case of GET is used later on*/
if (isAndroid) {
logger.debug("storing randomfile in session: " + FilenameUtils.getName(randomFilename));
session.setAttribute("tempFile", FilenameUtils.getName(randomFilename));
}
String output = convert(svg, mime, width, scale, options, constructor, callback, globalOptions, randomFilename);
ByteArrayOutputStream stream;
HttpHeaders headers = new HttpHeaders();
if (async) {
String link = TempDir.getDownloadLink(randomFilename);
stream = new ByteArrayOutputStream();
if (jsonp) {
StringBuilder sb = new StringBuilder(jsonpCallback);
sb.append("('");
sb.append(link);
sb.append("')");
stream.write(sb.toString().getBytes("utf-8"));
headers.add("Content-Type", "text/javascript; charset=utf-8");
} else {
stream.write(link.getBytes("utf-8"));
headers.add("Content-Type", "text/html; charset=UTF-8");
}
} else {
headers.add("Content-Type", mime.getType() + "; charset=utf-8");
if (randomFilename != null && randomFilename.equals(output)) {
stream = writeFileToStream(randomFilename);
} else {
boolean base64 = !mime.getExtension().equals("svg");
stream = outputToStream(output, base64);
}
filename = getFilename(filename);
headers.add("Content-Disposition",
"attachment; filename=" + filename.replace(" ", "_") + "." + mime.name().toLowerCase());
}
headers.setContentLength(stream.size());
return new HttpEntity<byte[]>(stream.toByteArray(), headers);
}
@RequestMapping(value = "/files/{name}.{ext}", method = RequestMethod.GET)
public HttpEntity<byte[]> getFile(@PathVariable("name") String name, @PathVariable("ext") String extension) throws SVGConverterException, IOException {
Path path = Paths.get(TempDir.getOutputDir().toString(), name + "." + extension);
String filename = path.toString();
MimeType mime = getMime(extension);
ByteArrayOutputStream stream = writeFileToStream(filename);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", mime.getType() + "; charset=utf-8");
headers.setContentLength(stream.size());
return new HttpEntity<byte[]>(stream.toByteArray(), headers);
}
@RequestMapping(value="/json/{name}.{ext}", method = RequestMethod.POST)
public HttpEntity<byte[]> exportFromJson(
@PathVariable("name") String name,
@PathVariable("ext") String extension,
@RequestBody String requestBody) throws SVGConverterException, TimeoutException, NoSuchElementException, PoolException {
String randomFilename;
randomFilename = null;
String json = requestBody;
MimeType mime = getMime(extension);
// add outfile parameter to the json with a simple string replace
if (MimeType.PDF.equals(mime)) {
randomFilename = createRandomFileName(mime.name().toLowerCase());
int ind = json.lastIndexOf("}");
json = new StringBuilder(json).replace(ind, ind+1,",\"outfile\":\"" + randomFilename).toString() + "\"}";
}
String result = converter.requestServer(json);
ByteArrayOutputStream stream;
if (randomFilename != null && randomFilename.equals(result)) {
stream = writeFileToStream(randomFilename);
} else {
boolean base64 = !mime.getExtension().equals("svg");
stream = outputToStream(result, base64);
}
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", mime.getType() + "; charset=utf-8");
headers.add("Content-Disposition",
"attachment; filename=" + name + "." + extension);
headers.setContentLength(stream.size());
return new HttpEntity<byte[]>(stream.toByteArray(), headers);
}
/*
* INSTANCE METHODS
*/
private String convert(String svg, MimeType mime, String width, String scale, String options, String constructor, String callback, String globalOptions, String filename) throws SVGConverterException, PoolException, NoSuchElementException, TimeoutException, ServletException {
Float parsedWidth = widthToFloat(width);
Float parsedScale = scaleToFloat(scale);
options = sanitize(options);
String input;
String output;
boolean convertSvg = false;
if (options != null) {
// create a svg file out of the options
input = options;
callback = sanitize(callback);
globalOptions = sanitize(globalOptions);
} else {
// assume SVG conversion
svg = sanitize(svg);
if (svg == null) {
logger.error("The mandatory 'svg' or 'options' POST parameter is undefined.");
throw new ServletException(
"The mandatory 'svg' or 'options' POST parameter is undefined.");
} else {
convertSvg = true;
input = svg;
}
}
if (convertSvg && mime.equals(MimeType.SVG)) {
output = converter.redirectSVG(input, filename);
} else {
output = converter.convert(input, mime, constructor, callback, globalOptions, parsedWidth, parsedScale, filename);
}
return output;
}
private String getFilename(String name) {
name = sanitize(name);
return (name != null) ? name : "chart";
}
private static MimeType getMime(String mime) {
MimeType type = MimeType.get(mime);
if (type != null) {
return type;
}
return MimeType.PNG;
}
private static String sanitize(String parameter) {
if (parameter == null || parameter.trim().isEmpty() || parameter.compareToIgnoreCase("undefined") == 0 || parameter.compareTo("null") == 0 || parameter.compareTo("{}") == 0) {
return null;
}
return parameter.trim();
}
private static Float widthToFloat(String width) throws SVGConverterException {
width = sanitize(width);
if (width != null) {
width = width.replace("px", "");
try {
Float parsedWidth = Float.valueOf(width);
if (parsedWidth.compareTo(MAX_WIDTH) > 0) {
return MAX_WIDTH;
}
if (parsedWidth.compareTo(0.0F) > 0) {
return parsedWidth;
}
} catch (NumberFormatException nfe) {
logger.error("Parameter width is wrong for value: " + width, nfe.fillInStackTrace());
throw new SVGConverterException("Parameter width is wrong for value: " + width);
}
}
return null;
}
private static Float scaleToFloat(String scale) throws SVGConverterException {
scale = sanitize(scale);
if (scale != null) {
try {
Float parsedScale = Float.valueOf(scale);
if (parsedScale.compareTo(MAX_SCALE) > 0) {
return MAX_SCALE;
} else if (parsedScale.compareTo(0.0F) > 0) {
return parsedScale;
}
} catch (NumberFormatException nfe) {
logger.error("Parameter scale is wrong for value: " + scale, nfe.fillInStackTrace());
throw new SVGConverterException("Parameter scale is wrong for value: " + scale);
}
}
return null;
}
public String createRandomFileName(String extension) {
Path path = Paths.get(TempDir.outputDir.toString(), RandomStringUtils.randomAlphanumeric(8) + "." + extension);
return path.toString();
}
private ByteArrayOutputStream outputToStream(String output, boolean base64) throws SVGConverterException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
if (base64) {
//decode the base64 string
stream.write(Base64.decodeBase64(output));
} else {
stream.write(output.getBytes());
}
} catch (IOException ex) {
logger.error("Error in outputToStream: " + ex.getMessage());
throw new SVGConverterException("Error while converting");
}
return stream;
}
private ByteArrayOutputStream writeFileToStream(String filename) throws SVGConverterException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
stream.write(FileUtils.readFileToByteArray(new File(filename)));
} catch (IOException ioex) {
logger.error("Tried to read file from filesystem: " + ioex.getMessage());
throw new SVGConverterException("IOException: cannot find your file to download...");
}
return stream;
}
/* EXCEPTION HANDLERS */
@ExceptionHandler(ZeroRequestParameterException.class)
public ModelAndView handleZeroRequestParameterException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("demo");
response.setStatus(302);
return modelAndView;
}
@ExceptionHandler(IOException.class)
public ModelAndView handleIOException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("message", ex.getMessage());
response.setStatus(500);
return modelAndView;
}
@ExceptionHandler(TimeoutException.class)
public ModelAndView handleTimeoutException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("message",
"Timeout converting SVG, is your file this big, or maybe you have a syntax error in the javascript callback?");
response.setStatus(500);
return modelAndView;
}
@ExceptionHandler(PoolException.class)
public ModelAndView handleServerPoolException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("message",
"Sorry, the server is handling too many requests at the moment. Please try again.");
response.setStatus(500);
return modelAndView;
}
@ExceptionHandler(SVGConverterException.class)
public ModelAndView handleSVGRasterizeException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("message",
"Something went wrong while converting. " + ex.getMessage());
response.setStatus(500);
return modelAndView;
}
@ExceptionHandler(InterruptedException.class)
public ModelAndView handleInterruptedException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("message",
"It took too long time to process the options, no SVG is created. Make sure your javascript is correct");
response.setStatus(500);
return modelAndView;
}
@ExceptionHandler(ServletException.class)
public ModelAndView handleServletException(Exception ex, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", ex.getMessage());
response.setStatus(500);
return modelAndView;
}
}