package com.belladati.demo.controller;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.belladati.demo.persist.ConfigData;
import com.belladati.demo.view.ViewAttribute;
import com.belladati.demo.view.ViewDisplay;
import com.belladati.sdk.dataset.Attribute;
import com.belladati.sdk.exception.server.MethodNotAllowedException;
import com.belladati.sdk.filter.Filter;
import com.belladati.sdk.filter.Filter.MultiValueFilter;
import com.belladati.sdk.filter.FilterOperation;
import com.belladati.sdk.filter.FilterValue;
import com.belladati.sdk.intervals.AbsoluteInterval;
import com.belladati.sdk.intervals.DateUnit;
import com.belladati.sdk.intervals.Interval;
import com.belladati.sdk.report.Comment;
import com.belladati.sdk.report.Report;
import com.belladati.sdk.view.View;
import com.belladati.sdk.view.ViewLoader;
import com.belladati.sdk.view.ViewType;
import com.belladati.sdk.view.export.ViewExport;
/**
* Controller handling reports.
*
* @author Chris Hennigfeld
*/
@Controller
public class ReportController {
private static final Logger logger = Logger.getLogger(ReportController.class.getName());
private static final String CONFIG_DATA = "config";
@Autowired
private ServiceManager serviceManager;
@Autowired
private HttpSession session;
/**
* Handles the root URL.
*/
@RequestMapping("/")
public ModelAndView initialUrl() {
if (serviceManager.isLoggedIn()) {
return new ModelAndView("redirect:/reports");
} else {
return new ModelAndView("redirect:/login");
}
}
/**
* Displays the report list.
*/
@RequestMapping("/reports")
public ModelAndView showReports() {
if (!serviceManager.isLoggedIn()) {
return new ModelAndView("redirect:/login");
}
ModelAndView modelAndView = new ModelAndView("reports");
// load report list from BellaDati, pass to view
modelAndView.addObject("reports", serviceManager.getService().getReportInfo().load().toList());
return modelAndView;
}
/**
* Displays a single report.
*
* @param reportId ID of the report to display
*/
@RequestMapping(value = "/reports/{id}", method = RequestMethod.GET)
public ModelAndView showReportViews(final @PathVariable("id") String reportId) throws ExecutionException,
InterruptedException {
if (!serviceManager.isLoggedIn()) {
return new ModelAndView("redirect:/login");
}
ModelAndView modelAndView = new ModelAndView("report");
Map<?, ?> sessionConfigData = (Map<?, ?>) session.getAttribute(CONFIG_DATA);
final ConfigData configData = sessionConfigData != null ? (ConfigData) sessionConfigData.get(reportId) : null;
final Report report = serviceManager.getService().loadReport(reportId);
modelAndView.addObject("reportName", report.getName());
modelAndView.addObject("reportId", report.getId());
// start executor service, submit various parallel queries
ExecutorService service = Executors.newCachedThreadPool();
// query view data
List<ViewDisplay> viewDisplays = new ArrayList<>();
for (final View view : report.getViews()) {
EnumSet<ViewType> supportedJsonViewTypes = EnumSet.of(ViewType.CHART,ViewType.KPI, ViewType.TABLE, ViewType.TEXT);
if (supportedJsonViewTypes.contains(view.getType())) {
Future<?> future = service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
ViewLoader loader = view.createLoader();
if (configData != null) {
loader.addFilters(configData.getFilters());
loader.setDateInterval(configData.getDateInterval());
loader.setTimeInterval(configData.getTimeInterval());
}
return loader.loadContent();
}
});
viewDisplays.add(new ViewDisplay(view, future));
} else if (view.getType() == ViewType.IMAGE) {
Future<?> future = service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
ViewLoader loader = view.createLoader();
return loader.loadContent();
}
});
viewDisplays.add(new ViewDisplay(view, future));
}
}
// query report comments
Future<List<Comment>> commentFuture = service.submit(new Callable<List<Comment>>() {
@Override
public List<Comment> call() throws Exception {
return report.getComments().load().toList();
}
});
// query attribute values
List<Future<Attribute>> attributeLoaders = new ArrayList<>();
for (final Attribute attribute : report.getAttributes()) {
if (isFilter(reportId, attribute)) {
attributeLoaders.add(service.submit(new Callable<Attribute>() {
@Override
public Attribute call() {
attribute.getValues().load();
return attribute;
}
}));
}
}
// once all queries are submitted, start processing their responses
// store view content
for (ViewDisplay viewDisplay : viewDisplays) {
viewDisplay.processFuture();
}
modelAndView.addObject("views", viewDisplays);
// store comments
modelAndView.addObject("comments", commentFuture.get());
// store attribute values
List<ViewAttribute> viewAttributes = new ArrayList<>();
for (Future<Attribute> future : attributeLoaders) {
Attribute attribute = future.get();
viewAttributes.add(new ViewAttribute(attribute));
}
// we're done with the executor
service.shutdown();
modelAndView.addObject("viewAttributes", viewAttributes);
if (configData != null) {
for (Filter<?> item : configData.getFilters()) {
for (ViewAttribute viewAttribute : viewAttributes) {
if (viewAttribute.getCode().equals(item.getAttribute().getCode())) {
List<String> values = new ArrayList<>();
for (com.belladati.sdk.dataset.AttributeValue value : ((MultiValueFilter) item).getValues()) {
values.add(value.getValue());
}
viewAttribute.setSelectedValues(values);
}
}
}
if (configData.getDateInterval() != null) {
DateFormat dateWriter = new SimpleDateFormat("yyyy-MM-dd");
modelAndView.addObject("fromDate",
dateWriter.format(((AbsoluteInterval<DateUnit>) configData.getDateInterval()).getStart().getTime()));
modelAndView.addObject("toDate",
dateWriter.format(((AbsoluteInterval<DateUnit>) configData.getDateInterval()).getEnd().getTime()));
}
}
return modelAndView;
}
/**
* Sets or clears the configuration for the given report.
*
* @param reportId ID of the report to configure
* @param action <tt>set</tt> to set a config, otherwise it's cleared
* @param request request containing config parameters
*/
@RequestMapping(value = "/reports/{id}", method = RequestMethod.POST)
public ModelAndView setConfig(@PathVariable("id") String reportId, @RequestParam("action") String action,
@RequestParam("fromDate") String fromDate, @RequestParam("toDate") String toDate, HttpServletRequest request) {
// read config storage from session, create if it doesn't exist
@SuppressWarnings("unchecked")
Map<String, ConfigData> sessionConfig = (Map<String, ConfigData>) session.getAttribute(CONFIG_DATA);
if (sessionConfig == null) {
sessionConfig = new HashMap<>();
session.setAttribute(CONFIG_DATA, sessionConfig);
}
// find config data for given report, create if it doesn't exist
ConfigData configData = sessionConfig.get(reportId);
if (configData == null) {
configData = new ConfigData(reportId);
sessionConfig.put(reportId, configData);
}
// create config collection, empty to clear
Collection<? extends Filter<?>> filters = new ArrayList<>();
if ("set".equals(action)) {
// we have something to filter
filters = parseFilters(reportId, request);
configData.setFilters(filters);
configData.setDateInterval(parseInterval(fromDate, toDate));
} else {
configData.clear();
}
return new ModelAndView("redirect:/reports/" + reportId);
}
/**
* Creates filters based on parameters set in the given request.
*
* @param reportId ID of the report to filter
* @param request request containing filter parameters
* @return filters from the given request
*/
private Collection<? extends Filter<?>> parseFilters(String reportId, HttpServletRequest request) {
// since we're using checkboxes, every attribute value comes out
// as a separate request parameter
// each value is formatted as CODE---VALUE
Map<String, MultiValueFilter> filters = new HashMap<>();
for (String param : request.getParameterMap().keySet()) {
if (param.contains("---")) {
String[] pieces = param.split("---");
// first piece is the code
String code = pieces[0];
// second piece is the value
String value = pieces[1];
if (!filters.containsKey(code)) {
// we don't have the code yet, create a new filter for it
filters.put(code, FilterOperation.IN.createFilter(serviceManager.getService(), reportId, code));
}
// add the value to the list of filtered values
filters.get(code).addValue(new FilterValue(value));
}
}
return filters.values();
}
/**
* Creates a date interval based on the given from and to dates. To create a
* valid interval, both date strings must be non-null and in yyyy-MM-dd
* format.
*
* @param fromDate first date of the interval
* @param toDate last date of the interval
* @return an interval, or <tt>null</tt> if any of the dates is invalid
*/
private Interval<DateUnit> parseInterval(String fromDate, String toDate) {
DateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
try {
start.setTime(parser.parse(fromDate));
end.setTime(parser.parse(toDate));
return new AbsoluteInterval<DateUnit>(DateUnit.DAY, start, end);
} catch (ParseException e) {
return null;
}
}
/**
* Loads the thumbnail image for the report with the given ID.
*
* @param id ID of the report
* @return the report's thumbnail, or <tt>null</tt> if no thumbnail is found
*/
@RequestMapping(value = "/reports/{id}/thumbnail", method = RequestMethod.GET, produces = "image/png")
@ResponseBody
public byte[] getThumbnail(@PathVariable String id) {
try {
BufferedImage thumbnail = (BufferedImage) serviceManager.getService().loadReportThumbnail(id);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(thumbnail, "png", baos);
baos.flush();
byte[] bytes = baos.toByteArray();
baos.close();
return bytes;
} catch (IOException e) {
logger.log(Level.WARNING, "Error loading image", e);
}
return null;
}
@RequestMapping(value = "/views/{viewId}/export/pdf", method = RequestMethod.GET, produces = "application/pdf")
@ResponseBody
public void exportViewToPdf(@PathVariable String viewId, HttpServletResponse response) {
ViewExport viewExport = null;
try {
viewExport = serviceManager.getService().createViewExporter().exportToPdf(viewId);
} catch(MethodNotAllowedException e) {
try {
response.sendError(405);
} catch (IOException e1) {
logger.log(Level.WARNING, "Error sending 405 http error", e1);
}
return;
}
try {
IOUtils.copy(viewExport.getInputStream(), response.getOutputStream());
} catch (IOException e) {
logger.log(Level.WARNING, "Error exporting to PDF", e);
}
}
/**
* Posts a comment to the report with the given ID.
*
* @param id ID of the report
* @param comment text of the comment
*/
@RequestMapping(value = "/comment/{id}", method = RequestMethod.POST)
public ModelAndView createComment(@PathVariable String id, @RequestParam("comment") String comment) {
serviceManager.getService().postComment(id, comment);
return new ModelAndView("redirect:/reports/" + id);
}
/**
* Returns a list of attribute codes used as filters for the given report.
*
* @param reportId ID of the report whose filters to find
* @return a list of attribute codes used as filters for the given report
*/
private List<String> getReportFilters(String reportId) {
// simple implementation; all reports use the same filters
return Arrays.asList("L_CITY", "L_PRODUCT");
}
/**
* Checks whether the given attribute may be used as a filter for the given
* report.
*
* @param reportId ID of the report whose filters to check
* @param attribute attribute that may be used for filtering
* @return <tt>true</tt> if the attribute can be used to filter the report
*/
private boolean isFilter(String reportId, Attribute attribute) {
for (String filterCode : getReportFilters(reportId)) {
if (filterCode.equals(attribute.getCode())) {
return true;
}
}
return false;
}
}