edu.jhuapl.openessence.controller.ReportController.java Source code

Java tutorial

Introduction

Here is the source code for edu.jhuapl.openessence.controller.ReportController.java

Source

/*
 * Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
 *                             All rights reserved.
 *
 * This material may be used, modified, or reproduced by or for the U.S.
 * Government pursuant to the rights granted under the clauses at
 * DFARS 252.227-7013/7014 or FAR 52.227-14.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * NO WARRANTY.   THIS MATERIAL IS PROVIDED "AS IS."  JHU/APL DISCLAIMS ALL
 * WARRANTIES IN THE MATERIAL, WHETHER EXPRESS OR IMPLIED, INCLUDING (BUT NOT
 * LIMITED TO) ANY AND ALL IMPLIED WARRANTIES OF PERFORMANCE,
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT OF
 * INTELLECTUAL PROPERTY RIGHTS. ANY USER OF THE MATERIAL ASSUMES THE ENTIRE
 * RISK AND LIABILITY FOR USING THE MATERIAL.  IN NO EVENT SHALL JHU/APL BE
 * LIABLE TO ANY USER OF THE MATERIAL FOR ANY ACTUAL, INDIRECT,
 * CONSEQUENTIAL, SPECIAL OR OTHER DAMAGES ARISING FROM THE USE OF, OR
 * INABILITY TO USE, THE MATERIAL, INCLUDING, BUT NOT LIMITED TO, ANY DAMAGES
 * FOR LOST PROFITS.
 */

package edu.jhuapl.openessence.controller;

import edu.jhuapl.bsp.detector.DetectorHelper;
import edu.jhuapl.bsp.detector.TemporalDetectorInterface;
import edu.jhuapl.bsp.detector.TemporalDetectorSimpleDataObject;
import edu.jhuapl.bsp.detector.temporal.epa.NoDetectorDetector;
import edu.jhuapl.graphs.Encoding;
import edu.jhuapl.graphs.GraphException;
import edu.jhuapl.graphs.GraphSource;
import edu.jhuapl.graphs.PointInterface;
import edu.jhuapl.graphs.controller.DefaultGraphData;
import edu.jhuapl.graphs.controller.GraphController;
import edu.jhuapl.graphs.controller.GraphDataHandlerInterface;
import edu.jhuapl.graphs.controller.GraphDataInterface;
import edu.jhuapl.graphs.controller.GraphDataSerializeToDiskHandler;
import edu.jhuapl.graphs.controller.GraphObject;
import edu.jhuapl.openessence.datasource.Dimension;
import edu.jhuapl.openessence.datasource.FieldType;
import edu.jhuapl.openessence.datasource.Filter;
import edu.jhuapl.openessence.datasource.OeDataSource;
import edu.jhuapl.openessence.datasource.OeDataSourceAccessException;
import edu.jhuapl.openessence.datasource.OeDataSourceException;
import edu.jhuapl.openessence.datasource.Record;
import edu.jhuapl.openessence.datasource.dataseries.AccumPoint;
import edu.jhuapl.openessence.datasource.dataseries.DataSeriesSource;
import edu.jhuapl.openessence.datasource.dataseries.Grouping;
import edu.jhuapl.openessence.datasource.dataseries.GroupingDimension;
import edu.jhuapl.openessence.datasource.jdbc.JdbcOeDataSource;
import edu.jhuapl.openessence.datasource.jdbc.QueryRecord;
import edu.jhuapl.openessence.datasource.jdbc.ResolutionHandler;
import edu.jhuapl.openessence.datasource.jdbc.dataseries.AccumPointImpl;
import edu.jhuapl.openessence.datasource.jdbc.dataseries.GroupingImpl;
import edu.jhuapl.openessence.datasource.jdbc.entry.JdbcOeDataEntrySource;
import edu.jhuapl.openessence.datasource.jdbc.filter.FieldFilter;
import edu.jhuapl.openessence.datasource.jdbc.filter.GteqFilter;
import edu.jhuapl.openessence.datasource.jdbc.filter.LteqFilter;
import edu.jhuapl.openessence.datasource.jdbc.filter.OneArgOpFilter;
import edu.jhuapl.openessence.datasource.jdbc.filter.sorting.OrderByFilter;
import edu.jhuapl.openessence.datasource.jdbc.timeresolution.sql.pgsql.PgSqlDateHelper;
import edu.jhuapl.openessence.datasource.jdbc.timeresolution.sql.pgsql.PgSqlWeeklyHandler;
import edu.jhuapl.openessence.datasource.ui.ChildTableConfiguration;
import edu.jhuapl.openessence.datasource.ui.DimensionConfiguration;
import edu.jhuapl.openessence.datasource.ui.PossibleValuesConfiguration;
import edu.jhuapl.openessence.i18n.InspectableResourceBundleMessageSource;
import edu.jhuapl.openessence.logging.LogStatements;
import edu.jhuapl.openessence.model.ChartData;
import edu.jhuapl.openessence.model.ChartModel;
import edu.jhuapl.openessence.model.DataSourceDetails;
import edu.jhuapl.openessence.model.TimeSeriesModel;
import edu.jhuapl.openessence.web.util.ControllerUtils;
import edu.jhuapl.openessence.web.util.DetailsQuery;
import edu.jhuapl.openessence.web.util.ErrorMessageException;
import edu.jhuapl.openessence.web.util.FileExportUtil;
import edu.jhuapl.openessence.web.util.Filters;
import edu.jhuapl.openessence.web.util.Sorters;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.lang.ArrayUtils;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonParser.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.NoSuchMessageException;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

import java.awt.*;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.security.Principal;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/report")
public class ReportController extends OeController {

    private static final int DEFAULT_LABEL_LENGTH = 45;
    private static final String PIE = "pie";
    private static final String BAR = "bar";

    private static final int DEFAULT_WEEK_STARTDAY = 1;
    private static final int DEFAULT_DAILY_PREPULL = 40;
    private static final Logger log = LoggerFactory.getLogger(ReportController.class);
    private static final Logger translationLog = LoggerFactory.getLogger("TranslationLogger");

    private DateFormat dateFormatDay = new SimpleDateFormat("yyyy-MM-dd");
    private DateFormat dateFormatWeek = new SimpleDateFormat("yyyy-MM-dd-'W'w");
    private DateFormat dateFormatWeekPart = new SimpleDateFormat("yyyy-MM-dd");
    private DateFormat dateFormatMonth = new SimpleDateFormat("yyyy-MM");
    private DateFormat dateFormatYear = new SimpleDateFormat("yyyy");

    private static final String DAILY = "daily";
    private static final String WEEKLY = "weekly";
    private static final String MONTHLY = "monthly";
    private static final String YEARLY = "yearly";
    private static final String TIMEZONE_ENABLED = "timezone.enabled";
    private String graphDir;

    @Resource
    private InspectableResourceBundleMessageSource messageSource;

    private Map<String, Integer> intervalMap;

    public ReportController() {
        intervalMap = new HashMap<String, Integer>();
        intervalMap.put("hourly", Calendar.HOUR_OF_DAY);
        intervalMap.put(DAILY, Calendar.DAY_OF_MONTH);
        intervalMap.put(WEEKLY, Calendar.WEEK_OF_YEAR);
        intervalMap.put(MONTHLY, Calendar.MONTH);
    }

    @Autowired
    public void setGraphDir(@Qualifier("graphDir") String graphDir) {
        this.graphDir = graphDir;
        File f = new File(this.graphDir);
        if (!f.exists()) {
            if (!f.mkdirs()) {
                log.error("Unable to make directory " + graphDir);
            }
        }
    }

    @RequestMapping("/getFields")
    // TODO return real domain object
    public @ResponseBody Map<String, Object> getFields(@RequestParam("dsId") JdbcOeDataSource ds)
            throws IOException {
        final Map<String, Object> result = new HashMap<String, Object>();
        final List<DimensionConfiguration> filters = new ArrayList<DimensionConfiguration>();
        filters.addAll(getDimensionsInformation(ds.getFilterDimensions()));

        if (ds instanceof DataSeriesSource) {
            final DataSeriesSource dss = (DataSeriesSource) ds;
            // Add reserved fields, accumId, resolution field, and resolutions
            // each available 'entry' is a 'possible value' in the combo

            if (ds.getFilterDimension("accumId") != null) {

                if (!dss.getAccumulations().isEmpty()) {
                    for (final DimensionConfiguration filter : filters) {
                        if ("accumId".equals(filter.getName())) {
                            final List<List<Object>> possvals = new ArrayList<List<Object>>();
                            for (final Dimension accum : dss.getAccumulations()) {
                                final LinkedList<Object> accEntry = new LinkedList<Object>();
                                // Id then display value
                                accEntry.add(accum.getId());
                                if (accum.getDisplayName() == null) {
                                    accEntry.add(accum.getId());
                                } else {
                                    accEntry.add(accum.getDisplayName());
                                }
                                possvals.add(accEntry);
                            }

                            filter.setPossibleValues(new PossibleValuesConfiguration(possvals));
                            break;
                        }
                    }

                    // Group/Resolutions
                    final DimensionConfiguration timeseriesGroupResolution = new DimensionConfiguration();
                    timeseriesGroupResolution.setName("timeseriesGroupResolution");
                    timeseriesGroupResolution.setType(FieldType.TEXT);

                    final Map<String, Object> formMetaData = new HashMap<String, Object>();
                    formMetaData.put("allowBlank", false);
                    final Map<String, Object> metaData = new HashMap<String, Object>();
                    metaData.put("form", formMetaData);

                    timeseriesGroupResolution.setMeta(metaData);

                    final List<List<Object>> groupResolutionValues = new ArrayList<List<Object>>();
                    for (final GroupingDimension gdim : dss.getGroupingDimensions()) {
                        for (final String res : gdim.getResolutions()) {
                            final LinkedList<Object> groupResolutionEntry = new LinkedList<Object>();
                            // Id then display value
                            groupResolutionEntry.add(gdim.getId() + ":" + res);
                            groupResolutionEntry.add(messageSource.getDataSourceMessage(gdim.getId(), dss) + " / "
                                    + messageSource.getDataSourceMessage(res, dss));
                            groupResolutionValues.add(groupResolutionEntry);
                        }
                    }
                    timeseriesGroupResolution
                            .setPossibleValues(new PossibleValuesConfiguration(groupResolutionValues));
                    filters.add(timeseriesGroupResolution);
                }
            }
        }
        result.put("filters", filters);

        //Updated detail dimensions to use new dimension configuration that populates
        //possibleValues
        List<DimensionConfiguration> detailDimensions = new ArrayList<DimensionConfiguration>();
        detailDimensions.addAll(getDimensionsInformation(ds.getResultDimensions()));
        result.put("detailDimensions", detailDimensions);

        if (ds instanceof JdbcOeDataEntrySource) {
            final JdbcOeDataEntrySource jdes = (JdbcOeDataEntrySource) ds;
            result.put("pks", jdes.getParentTableDetails().getPks());
            final ArrayList<Object> editDimensions = new ArrayList<Object>();
            editDimensions.addAll(getDimensionsInformation(jdes.getEditDimensions()));

            // Add child table information
            for (final String tableName : jdes.getChildTableMap().keySet()) {
                editDimensions.add(new ChildTableConfiguration(tableName, jdes));
            }

            result.put("editDimensions", editDimensions);
        }

        // Data source level meta data
        final Map<String, Object> meta = ds.getMetaData();
        if (meta != null) {
            result.put("meta", meta);
        }

        result.put("success", true);

        return result;
    }

    private List<DimensionConfiguration> getDimensionsInformation(final Collection<? extends Dimension> dimensions)
            throws OeDataSourceException {
        List<DimensionConfiguration> results = new ArrayList<DimensionConfiguration>();

        for (final Dimension dimension : dimensions) {
            results.add(new DimensionConfiguration(dimension));
        }
        return results;
    }

    @RequestMapping("/timeSeriesJson")
    public @ResponseBody Map<String, Object> timeSeriesJson(@RequestParam("dsId") JdbcOeDataSource ds,
            TimeSeriesModel model, Principal principal, WebRequest request, HttpServletRequest servletRequest)
            throws ErrorMessageException {

        Map<String, Object> result = new HashMap<String, Object>();

        DataSeriesSource dss = null;
        if (ds instanceof DataSeriesSource) {
            dss = (DataSeriesSource) ds;

        }

        String groupId = "";
        String resolution = "";

        // TODO put this logic in a custom HandlerMethodArgumentResolver
        if (model.getTimeseriesGroupResolution() != null) {
            String[] parts = model.getTimeseriesGroupResolution().split(":");
            if (parts.length == 2 && !parts[0].trim().isEmpty() && !parts[1].trim().isEmpty()) {
                groupId = parts[0];
                resolution = parts[1];
            }
        }

        if (groupId == null || groupId.isEmpty()) {
            throw new OeDataSourceException("No Grouping Dimension ID specified");
        }

        GroupingDimension groupingDim = dss.getGroupingDimension(groupId);
        // find resolution handlers as appropriate for groupings
        if (resolution == null || "".equals(groupId)) {
            String[] res = groupingDim.getResolutions().toArray(new String[groupingDim.getResolutions().size()]);
            if (res.length > 0) {
                resolution = res[0];
            }
        }

        if (ds.getDimensionJoiner() != null) {
            ds.getDimensionJoiner().joinDimensions();
        }

        List<Dimension> accumulations = ControllerUtils.getAccumulationsByIds(ds, model.getAccumId());
        List<Dimension> timeseriesDenominators = ControllerUtils.getAccumulationsByIds(ds,
                model.getTimeseriesDenominator(), false);

        final List<OrderByFilter> sorts = new ArrayList<OrderByFilter>();
        GroupingImpl group = new GroupingImpl(groupId, resolution);

        if (resolution.equals(DAILY) && model.getPrepull() < 0) {
            model.setPrepull(DEFAULT_DAILY_PREPULL);
        }

        if (model.getPrepull() < 0) {
            model.setPrepull(0);
        }

        //union accumulations to get all results
        List<Dimension> dimensions = new ArrayList<Dimension>(
                ControllerUtils.unionDimensions(accumulations, timeseriesDenominators));
        List<Grouping> groupings = new ArrayList<Grouping>();
        groupings.add(groupingDim.makeGrouping(resolution));

        //create results group dimension + all dimensions
        final List<Dimension> results = new ArrayList<Dimension>();
        for (Dimension d : dimensions) {
            results.add(ds.getResultDimension(d.getId()));
        }

        Map<String, ResolutionHandler> resolutionHandlers = dss.getGroupingDimension(group.getId())
                .getResolutionsMap();
        List<Filter> filters = new Filters().getFilters(request.getParameterMap(), dss, group.getId(),
                model.getPrepull(), resolution, getCalWeekStartDay(resolutionHandlers));

        String clientTimezone = null;
        String timezoneEnabledString = messageSource.getMessage(TIMEZONE_ENABLED, "false");
        if (timezoneEnabledString.equalsIgnoreCase("true")) {
            clientTimezone = ControllerUtils.getRequestTimezoneAsHourMinuteString(request);
        }
        //details query for all records
        Collection<Record> records = new DetailsQuery().performDetailsQuery(ds, results, dimensions, filters, sorts,
                groupings, false, clientTimezone);

        //create graph data and set known configuration
        DefaultGraphData graphData = new DefaultGraphData();
        graphData.setShowSingleSeverityLegends(false);
        graphData.setGraphTitle(model.getTimeseriesTitle());
        graphData.setGraphWidth(model.getWidth());
        graphData.setGraphHeight(model.getHeight());
        graphData.setShowLegend(true);
        graphData.setBackgroundColor(new Color(255, 255, 255, 0));

        // only set an array if they provided one
        if (model.getGraphBaseColors() != null && model.getGraphBaseColors().length > 0) {
            // TODO leverage Spring to convert colors
            graphData.setGraphBaseColors(ControllerUtils.getColorsFromHex(Color.BLACK, model.getGraphBaseColors()));
        }

        String graphTimeSeriesUrl = request.getContextPath() + servletRequest.getServletPath()
                + "/report/graphTimeSeries";
        graphTimeSeriesUrl = appendGraphFontParam(ds, graphTimeSeriesUrl);

        //TODO, this still uses the html method from the graph module and then wraps in json...move to a pure json method
        Map<String, Object> timeseriesResult = createTimeseries(principal.getName(), dss, filters, group,
                resolution, model.getPrepull(), graphTimeSeriesUrl, records, accumulations, timeseriesDenominators,
                model.getTimeseriesDetectorClass(), model.isIncludeDetails(), model.isDisplayIntervalEndDate(),
                graphData, ControllerUtils.getRequestTimezone(request));

        result.putAll(timeseriesResult);

        return result;
    }

    @RequestMapping("/chartJson")
    public @ResponseBody Map<String, Object> chartJson(WebRequest request, HttpServletRequest servletRequest,
            @RequestParam("dsId") JdbcOeDataSource ds, ChartModel chartModel) throws ErrorMessageException {

        log.info(LogStatements.GRAPHING.getLoggingStmt() + request.getUserPrincipal().getName());

        final List<Filter> filters = new Filters().getFilters(request.getParameterMap(), ds, null, 0, null, 0);
        final List<Dimension> results = ControllerUtils.getResultDimensionsByIds(ds,
                request.getParameterValues("results"));

        Dimension filterDimension = null;
        if (results.get(0).getFilterBeanId() != null && results.get(0).getFilterBeanId().length() > 0) {
            filterDimension = ds.getFilterDimension(results.get(0).getFilterBeanId());
        }
        // if not provided, use the result dimension
        // it means name and id columns are same...
        if (filterDimension != null) {
            results.add(results.size(), filterDimension);
        }

        // Subset of results, should check
        final List<Dimension> charts = ControllerUtils.getResultDimensionsByIds(ds,
                request.getParameterValues("charts"));

        final List<Dimension> accumulations = ControllerUtils.getAccumulationsByIds(ds,
                request.getParameterValues("accumId"));

        final List<OrderByFilter> sorts = new ArrayList<OrderByFilter>();
        try {
            sorts.addAll(Sorters.getSorters(request.getParameterMap()));
        } catch (Exception e) {
            log.warn("Unable to get sorters, using default ordering");
        }

        // TODO put this on ChartModel
        //default to white allows clean copy paste of charts from browser
        Color backgroundColor = Color.WHITE;

        String bgParam = request.getParameter("backgroundColor");
        if (bgParam != null && !"".equals(bgParam)) {
            if ("transparent".equalsIgnoreCase(bgParam)) {
                backgroundColor = new Color(255, 255, 255, 0);
            } else {
                backgroundColor = ControllerUtils.getColorsFromHex(Color.WHITE, bgParam)[0];
            }
        }

        String graphBarUrl = request.getContextPath() + servletRequest.getServletPath() + "/report/graphBar";
        graphBarUrl = appendGraphFontParam(ds, graphBarUrl);

        String graphPieUrl = request.getContextPath() + servletRequest.getServletPath() + "/report/graphPie";
        graphPieUrl = appendGraphFontParam(ds, graphPieUrl);

        // TODO eliminate all the nesting in response and just use accumulation and chartID properties
        Map<String, Object> response = new HashMap<String, Object>();
        Map<String, Object> graphs = new HashMap<String, Object>();
        response.put("graphs", graphs);

        String clientTimezone = null;
        String timezoneEnabledString = messageSource.getMessage(TIMEZONE_ENABLED, "false");
        if (timezoneEnabledString.equalsIgnoreCase("true")) {
            clientTimezone = ControllerUtils.getRequestTimezoneAsHourMinuteString(request);
        }
        Collection<Record> records = new DetailsQuery().performDetailsQuery(ds, results, accumulations, filters,
                sorts, false, clientTimezone);
        final List<Filter> graphFilters = new Filters().getFilters(request.getParameterMap(), ds, null, 0, null, 0,
                false);
        //for each requested accumulation go through each requested result and create a chart
        for (Dimension accumulation : accumulations) {
            Map<String, Object> accumulationMap = new HashMap<String, Object>();
            // Create charts for dimensions (subset of results)
            for (Dimension chart : charts) {
                DefaultGraphData data = new DefaultGraphData();
                data.setGraphTitle(chartModel.getTitle());
                data.setGraphHeight(chartModel.getHeight());
                data.setGraphWidth(chartModel.getWidth());
                data.setShowLegend(chartModel.isLegend());
                data.setBackgroundColor(backgroundColor);
                data.setShowGraphLabels(chartModel.isShowGraphLabels());
                data.setLabelBackgroundColor(backgroundColor);
                data.setPlotHorizontal(chartModel.isPlotHorizontal());
                data.setNoDataMessage(chartModel.getNoDataMessage());
                data.setTitleFont(new Font("Arial", Font.BOLD, 12));

                GraphObject graph = createGraph(ds, request.getUserPrincipal().getName(), records, chart,
                        filterDimension, accumulation, data, chartModel, graphFilters);
                String graphURL = "";
                if (BAR.equalsIgnoreCase(chartModel.getType())) {
                    graphURL = graphBarUrl;
                } else if (PIE.equalsIgnoreCase(chartModel.getType())) {
                    graphURL = graphPieUrl;
                }
                graphURL = appendUrlParameter(graphURL, "graphDataId", graph.getGraphDataId());

                chartModel.setImageUrl(graphURL);
                chartModel.setImageMap(graph.getImageMap());
                chartModel.setImageMapName(graph.getImageMapName());

                accumulationMap.put(chart.getId(), chartModel);
            }
            graphs.put(accumulation.getId(), accumulationMap);
        }

        log.info(String.format("Chart JSON Details query for %s", request.getUserPrincipal().getName()));

        return response;
    }

    private Map<String, Object> createTimeseries(String userPrincipalName, DataSeriesSource dss,
            List<Filter> filters, GroupingImpl group, String timeResolution, Integer prepull,
            String graphTimeSeriesUrl, final Collection<Record> records, final List<Dimension> accumulations,
            final List<Dimension> timeseriesDenominators, String detectorClass, boolean includeDetails,
            boolean displayIntervalEndDate, GraphDataInterface graphData, TimeZone clientTimezone) {

        Map<String, Object> result = new HashMap<String, Object>();
        Map<String, ResolutionHandler> resolutionHandlers = null;
        result.put("success", false);
        try {
            GroupingDimension grpdim = dss.getGroupingDimension(group.getId());
            resolutionHandlers = grpdim.getResolutionsMap();
            String dateFieldName = group.getId();
            Date startDate = null;
            Date endDate = null;
            if (grpdim != null
                    && (grpdim.getSqlType() == FieldType.DATE || grpdim.getSqlType() == FieldType.DATE_TIME)) {
                for (Filter f : filters) {
                    if (f instanceof OneArgOpFilter) {
                        OneArgOpFilter of = (OneArgOpFilter) f;
                        if (of.getFilterId().equalsIgnoreCase(grpdim.getId())
                                && (of.getSqlSnippet("").contains(">="))) {
                            startDate = (Date) of.getArguments().get(0);
                        } else if (of.getFilterId().equalsIgnoreCase(grpdim.getId())
                                && (of.getSqlSnippet("").contains("<="))) {
                            endDate = (Date) of.getArguments().get(0);
                        }
                    }
                }
            }
            //union accumulations to get all results
            List<Dimension> dimensions = new ArrayList<Dimension>(
                    ControllerUtils.unionDimensions(accumulations, timeseriesDenominators));

            int timeOffsetMillies = 0;
            String timezoneEnabledString = messageSource.getMessage(TIMEZONE_ENABLED, "false");
            if (timezoneEnabledString.equalsIgnoreCase("true")) {
                timeOffsetMillies = (clientTimezone.getRawOffset() - clientTimezone.getDSTSavings())
                        - (TimeZone.getDefault().getRawOffset() - TimeZone.getDefault().getDSTSavings());
            }
            Calendar startDayCal = Calendar.getInstance(clientTimezone);
            startDayCal.setTime(startDate);
            startDayCal.add(Calendar.MILLISECOND, timeOffsetMillies);

            //get data grouped by group dimension
            List<AccumPoint> points = extractAccumulationPoints(userPrincipalName, dss, records,
                    startDayCal.getTime(), endDate, dimensions, group, resolutionHandlers);
            if (points.size() > 0) {
                DateFormat dateFormat = getDateFormat(timeResolution); //dateFormat.setTimeZone(timezone);
                DateFormat tmpDateFormat = (DateFormat) dateFormat.clone();
                tmpDateFormat.setTimeZone(clientTimezone);

                // number format for level
                NumberFormat numFormat3 = NumberFormat.getNumberInstance();
                numFormat3.setMinimumFractionDigits(0);
                numFormat3.setMaximumFractionDigits(3);

                // number format for expected count
                NumberFormat numFormat1 = NumberFormat.getNumberInstance();
                numFormat1.setMinimumFractionDigits(0);
                numFormat1.setMaximumFractionDigits(1);

                Calendar cal = new GregorianCalendar();
                cal.setTime(startDayCal.getTime());
                //offset start date to match prepull offset
                if (timeResolution.equals("weekly")) {
                    cal.add(Calendar.DATE, (7 * prepull));
                } else if (timeResolution.equals("daily")) {
                    cal.add(Calendar.DATE, prepull);
                }
                Date queryStartDate = cal.getTime();

                //-- Handles Denominator Types -- //
                double[] divisors = new double[points.size()];
                double multiplier = 1.0;
                boolean percentBased = false;
                String yAxisLabel = messageSource.getDataSourceMessage("graph.count", dss);

                boolean isDetectionDetector = !NoDetectorDetector.class.getName().equalsIgnoreCase(detectorClass);

                //if there is a denominator we need to further manipulate the data
                if (timeseriesDenominators != null && !timeseriesDenominators.isEmpty()) {
                    // divisor is the sum of timeseriesDenominators
                    divisors = totalSeriesValues(points, timeseriesDenominators);
                    multiplier = 100.0;
                    percentBased = true;
                    yAxisLabel = messageSource.getDataSourceMessage("graph.percent", dss);
                } else {
                    //the query is for total counts
                    Arrays.fill(divisors, 1.0);
                }

                double[][] allCounts = new double[accumulations.size()][];
                int[][] allColors = new int[accumulations.size()][];
                String[][] allAltTexts = new String[accumulations.size()][];
                String[] dates = new String[] { "" };
                double[][] allExpecteds = new double[accumulations.size()][];
                double[][] allLevels = new double[accumulations.size()][];
                String[][] allLineSetURLs = new String[accumulations.size()][];
                String[][] allSwitchInfo = new String[accumulations.size()][];
                String[] lineSetLabels = new String[accumulations.size()];
                boolean[] displayAlerts = new boolean[accumulations.size()];

                //get all results
                Collection<Dimension> dims = new ArrayList<Dimension>(dss.getResultDimensions());
                Collection<String> dimIds = ControllerUtils.getDimensionIdsFromCollection(dims);
                Collection<String> accIds = ControllerUtils.getDimensionIdsFromCollection(dss.getAccumulations());
                //remove extra accumulations in the result set using string ids
                dimIds.removeAll(accIds);

                //for each accumulation we run detection and gather results
                int aIndex = 0;
                for (Dimension accumulation : accumulations) {
                    String accumId = accumulation.getId();

                    // use display name if it has one, otherwise translate its ID
                    String accumIdTranslated = accumulation.getDisplayName();
                    if (accumIdTranslated == null) {
                        accumIdTranslated = messageSource.getDataSourceMessage(accumulation.getId(), dss);
                    }

                    TemporalDetectorInterface TDI = (TemporalDetectorInterface) DetectorHelper
                            .createObject(detectorClass);
                    TemporalDetectorSimpleDataObject TDDO = new TemporalDetectorSimpleDataObject();

                    int[] colors;
                    double[] counts;
                    String[] altTexts;
                    double[] expecteds;
                    double[] levels;
                    String[] switchInfo;
                    String[] urls;

                    //pull the counts from the accum array points
                    double[] seriesDoubleArray = generateSeriesValues(points, accumId);

                    //run divisor before detection
                    for (int i = 0; i < seriesDoubleArray.length; i++) {
                        double div = divisors[i];
                        if (div == 0) {
                            seriesDoubleArray[i] = 0.0;
                        } else {
                            seriesDoubleArray[i] = (seriesDoubleArray[i] / div) * multiplier;
                        }
                    }

                    //run detection
                    TDDO.setCounts(seriesDoubleArray);
                    TDDO.setStartDate(startDate);
                    TDDO.setTimeResolution(timeResolution);

                    try {
                        TDI.runDetector(TDDO);
                    } catch (Exception e) {
                        String errorMessage = "Failure to create Timeseries";
                        if (e.getMessage() != null) {
                            errorMessage = errorMessage + ":<BR>" + e.getMessage();
                        }
                        result.put("message", errorMessage);
                        result.put("success", false);
                        return result;
                    }

                    TDDO.cropStartup(prepull);
                    counts = TDDO.getCounts();
                    int tddoLength = counts.length;

                    if (!DAILY.equalsIgnoreCase(timeResolution)) {
                        //toggle between start date and end date
                        //TDDO.setDates(getOurDates(startDate, endDate, tddoLength, timeResolution));
                        TDDO.setDates(getOurDates(queryStartDate, endDate, tddoLength, timeResolution,
                                displayIntervalEndDate));
                    }
                    double[] tcolors = TDDO.getColors();

                    Date[] tdates = TDDO.getDates();
                    altTexts = TDDO.getAltTexts();
                    expecteds = TDDO.getExpecteds();
                    levels = TDDO.getLevels();
                    switchInfo = TDDO.getSwitchInfo();
                    colors = new int[tddoLength];
                    dates = new String[tddoLength];
                    urls = new String[tddoLength];

                    //add the accumId for the current series
                    dimIds.add(accumId);

                    StringBuilder jsCall = new StringBuilder();
                    jsCall.append("javascript:OE.report.datasource.showDetails({");
                    jsCall.append("dsId:'").append(dss.getClass().getName()).append("'");
                    //specify results
                    jsCall.append(",results:[")
                            .append(StringUtils.collectionToDelimitedString(dimIds, ",", "'", "'")).append(']');
                    //specify accumId
                    jsCall.append(",accumId:'").append(accumId).append("'");

                    addJavaScriptFilters(jsCall, filters, dateFieldName);

                    //this builds urls and hover texts
                    int startDay = getWeekStartDay(resolutionHandlers);

                    Calendar c = Calendar.getInstance(clientTimezone);

                    //               Calendar curr = Calendar.getInstance();
                    for (int i = 0; i < tddoLength; i++) {
                        colors[i] = (int) tcolors[i];

                        // For a time series data point, set time to be current server time
                        // This will allow us to convert this data point date object to be request timezone date
                        c.setTime(tdates[i]);
                        c.add(Calendar.MILLISECOND, timeOffsetMillies);

                        if (timeResolution.equals(WEEKLY)) {
                            dates[i] = dateFormatWeekPart.format(tdates[i]) + "-W"
                                    + PgSqlDateHelper.getWeekOfYear(startDay, c) + "-"
                                    + PgSqlDateHelper.getYear(startDay, c);
                        } else {
                            dates[i] = tmpDateFormat.format(c.getTime());
                        }

                        altTexts[i] = "(" + accumIdTranslated + ") " + // Accum
                                "Date: " + dates[i] + // Date
                                ", Level: " + numFormat3.format(levels[i]) + // Level
                                ", Count: " + ((int) counts[i]) + // Count
                                ", Expected: " + numFormat1.format(expecteds[i]); // Expected

                        if (switchInfo != null) {
                            altTexts[i] += ", Switch: " + switchInfo[i] + ", ";
                        }

                        // build the click through url
                        StringBuilder tmp = new StringBuilder(jsCall.toString());

                        // add the date field with start and end dates from the data point
                        if (!DAILY.equalsIgnoreCase(timeResolution)) {
                            Calendar timeSet = Calendar.getInstance(clientTimezone);
                            timeSet.setTime(tdates[i]);

                            if (WEEKLY.equalsIgnoreCase(timeResolution)) {
                                timeSet.set(Calendar.DAY_OF_WEEK, startDay + 1);
                                tmp.append(",").append(dateFieldName).append("_start:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                                timeSet.add(Calendar.DAY_OF_YEAR, 6);
                                tmp.append(",").append(dateFieldName).append("_end:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                            } else if (MONTHLY.equalsIgnoreCase(timeResolution)) {
                                // Compute last day of month
                                timeSet.set(Calendar.DAY_OF_MONTH, 1);
                                timeSet.add(Calendar.MONTH, 1);
                                timeSet.add(Calendar.DAY_OF_YEAR, -1);
                                tmp.append(",").append(dateFieldName).append("_end:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                                // set first day of month
                                timeSet.set(Calendar.DAY_OF_MONTH, 1);
                                tmp.append(",").append(dateFieldName).append("_start:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                            } else if (YEARLY.equalsIgnoreCase(timeResolution)) {
                                // Compute last day of month
                                timeSet.set(Calendar.DATE, 31);
                                timeSet.add(Calendar.MONTH, Calendar.DECEMBER);
                                tmp.append(",").append(dateFieldName).append("_end:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                                timeSet.set(Calendar.DATE, 1);
                                timeSet.add(Calendar.MONTH, Calendar.JANUARY);
                                tmp.append(",").append(dateFieldName).append("_start:'")
                                        .append(timeSet.getTimeInMillis()).append("'");
                            }
                        } else {
                            // compute end date for individual data points based on the selected resolution
                            //                     detailsPointEndDate = computeEndDate(tdates[i],timeResolution);
                            // add the date field with start and end dates from the data point
                            tmp.append(",").append(dateFieldName).append("_start:'").append(tdates[i].getTime())
                                    .append("'");
                            tmp.append(",").append(dateFieldName).append("_end:'").append(tdates[i].getTime())
                                    .append("'");
                        }
                        tmp.append("});");
                        urls[i] = tmp.toString();
                    }

                    allCounts[aIndex] = counts;
                    allColors[aIndex] = colors;
                    allAltTexts[aIndex] = altTexts;
                    allExpecteds[aIndex] = expecteds;
                    allLevels[aIndex] = levels;
                    allLineSetURLs[aIndex] = urls;
                    allSwitchInfo[aIndex] = switchInfo;
                    lineSetLabels[aIndex] = accumIdTranslated;
                    displayAlerts[aIndex] = isDetectionDetector;
                    aIndex++;

                    //remove the accumId for the next series
                    dimIds.remove(accumId);
                }

                GraphDataSerializeToDiskHandler hndl = new GraphDataSerializeToDiskHandler(graphDir);
                GraphController gc = getGraphController(null, hndl, userPrincipalName);
                //TODO figure out why I (hodancj1) added this to be accumulation size ~Feb 2012
                // gc.setMaxLegendItems(accumulations.size());

                graphData.setShowSingleAlertLegends(isDetectionDetector);
                graphData.setCounts(allCounts);
                graphData.setColors(allColors);
                graphData.setAltTexts(allAltTexts);
                graphData.setXLabels(dates);
                graphData.setExpecteds(allExpecteds);
                graphData.setLevels(allLevels);
                graphData.setLineSetURLs(allLineSetURLs);
                graphData.setLineSetLabels(lineSetLabels);
                graphData.setDisplayAlerts(displayAlerts);
                // graphData.setDisplaySeverityAlerts(displayAlerts);
                graphData.setPercentBased(percentBased);

                graphData.setXAxisLabel(messageSource.getDataSourceMessage(group.getResolution(), dss));
                graphData.setYAxisLabel(yAxisLabel);

                int maxLabels = graphData.getGraphWidth() / 30;
                graphData.setMaxLabeledCategoryTicks(Math.min(maxLabels, allCounts[0].length));

                StringBuffer sb = new StringBuffer();
                GraphObject graph = gc.writeTimeSeriesGraph(sb, graphData, true, true, false, graphTimeSeriesUrl);

                result.put("html", sb.toString());

                //added to build method calls from javascript
                Map<String, Object> graphConfig = new HashMap<String, Object>();
                graphConfig.put("address", graphTimeSeriesUrl);
                graphConfig.put("graphDataId", graph.getGraphDataId());
                graphConfig.put("imageMapName", graph.getImageMapName());

                graphConfig.put("graphTitle", graphData.getGraphTitle());
                graphConfig.put("xAxisLabel", graphData.getXAxisLabel());
                graphConfig.put("yAxisLabel", graphData.getYAxisLabel());
                graphConfig.put("xLabels", graphData.getXLabels());
                graphConfig.put("graphWidth", graphData.getGraphWidth());
                graphConfig.put("graphHeight", graphData.getGraphHeight());

                graphConfig.put("yAxisMin", graph.getYAxisMin());
                graphConfig.put("yAxisMax", graph.getYAxisMax());

                // fix invalid JSON coming from GraphController
                String dataSeriesJson = graph.getDataSeriesJSON().replaceFirst("\\{", "")
                        // remove trailing "}"
                        .substring(0, graph.getDataSeriesJSON().length() - 2);

                // read malformed JSON
                ObjectMapper mapper = new ObjectMapper();
                JsonFactory jsonFactory = mapper.getJsonFactory()
                        .configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
                        .configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
                JsonParser jsonParser = jsonFactory.createJsonParser(dataSeriesJson);

                // array of String -> Object maps
                TypeReference<Map<String, Object>[]> dataSeriesType = new TypeReference<Map<String, Object>[]>() {
                };

                // write JSON as Map so that it can be serialized properly back to JSON
                Map<String, Object>[] seriesMap = mapper.readValue(jsonParser, dataSeriesType);
                graphConfig.put("dataSeriesJSON", seriesMap);

                if (includeDetails) {
                    int totalPoints = 0;
                    List<HashMap<String, Object>> details = new ArrayList<HashMap<String, Object>>();
                    HashMap<String, Object> detail;
                    for (int i = 0; i < allCounts.length; i++) {
                        for (int j = 0; j < allCounts[i].length; j++) {
                            totalPoints++;
                            detail = new HashMap<String, Object>();
                            detail.put("Date", dates[j]);
                            detail.put("Series", lineSetLabels[i]);
                            detail.put("Level", allLevels[i][j]);
                            detail.put("Count", allCounts[i][j]);
                            if (!ArrayUtils.isEmpty(allExpecteds[i])) {
                                detail.put("Expected", allExpecteds[i][j]);
                            }
                            if (!ArrayUtils.isEmpty(allSwitchInfo[i])) {
                                detail.put("Switch", allSwitchInfo[i][j]);
                            }
                            detail.put("Color", allColors[i][j]);
                            details.add(detail);
                        }
                    }
                    result.put("detailsTotalRows", totalPoints);
                    result.put("details", details);
                }
                result.put("graphConfiguration", graphConfig);
                result.put("success", true);
            } else {
                StringBuilder sb = new StringBuilder();
                sb.append("<h2>" + messageSource.getDataSourceMessage("graph.nodataline1", dss) + "</h2>");
                sb.append("<p>" + messageSource.getDataSourceMessage("graph.nodataline2", dss) + "</p>");
                result.put("html", sb.toString());
                result.put("success", true);
            }
        } catch (Exception e) {
            log.error("Failure to create Timeseries", e);
        }
        return result;
    }

    protected DateFormat getDateFormat(String timeResolution) {
        DateFormat dateFormat = dateFormatDay;
        String formatKey = "java.date.formatDay";
        String formatError = "[" + formatKey + "]";
        String formatValue = "";
        if (DAILY.equalsIgnoreCase(timeResolution)) {
            formatKey = "java.date.formatDay";
            dateFormat = dateFormatDay;
        } else if (WEEKLY.equalsIgnoreCase(timeResolution)) {
            formatKey = "java.date.formatWeek";
            dateFormat = dateFormatWeek;
        } else if (MONTHLY.equalsIgnoreCase(timeResolution)) {
            formatKey = "java.date.formatMonth";
            dateFormat = dateFormatMonth;
        } else if (YEARLY.equalsIgnoreCase(timeResolution)) {
            formatKey = "java.date.formatYear";
            dateFormat = dateFormatYear;
        }
        formatError = "[" + formatKey + "]";
        formatValue = messageSource.getMessage(formatKey);

        if (!"".equals(formatValue) && !formatError.equals(formatValue)) {
            try {
                dateFormat = new SimpleDateFormat(formatValue);
            } catch (Exception ex) {
                translationLog.error("Error parsing " + formatKey + " into a DateFormat", ex);
            }
        }
        return dateFormat;
    }

    //Original code taken from TemporalDetectorSimpleDataObserver.setupDates and reworked to better handle months
    private Date[] getOurDates(Date queryStartDate, Date endDate, int size, String timeResolution,
            boolean displayIntervalEndDate) {
        Date startDate = queryStartDate;
        if (displayIntervalEndDate) {
            startDate = computeResolutionBasedEndDate(queryStartDate, timeResolution, endDate);
        }

        Date[] dates = new Date[size];

        String tr = timeResolution;
        if (tr == null) {
            tr = DAILY;
        }
        int zeroFillInterval = intervalMap.keySet().contains(timeResolution) ? intervalMap.get(timeResolution) : -1;
        if (startDate != null && size >= 0) {
            Calendar cal = new GregorianCalendar();
            //forward point allows us to place the accumulated data at the front
            int i = 0;
            for (i = 0; i < size; i++) {
                //reset date to avoid unexpected date changes
                cal.setTime(startDate);
                cal.add(zeroFillInterval, 1 * i);
                if (endDate != null && cal.getTime().after(endDate)) {
                    cal.setTime(endDate);
                }
                //store date after interval addition
                dates[i] = cal.getTime();
            }
        }
        return dates;
    }

    /**
     * Compute end date based on time resolution. Defaults to the original date unless the resolution is weekly, monthly
     * or yearly in which case it is padded accordingly.
     *
     * @param maxDate optionally used to keep the computed date below a maxDate (end date for the query for example)
     * @return Date
     */
    private Date computeResolutionBasedEndDate(Date startDate, String timeResolution, Date maxDate) {
        Calendar cal = new GregorianCalendar();
        cal.setTime(startDate);
        if (WEEKLY.equalsIgnoreCase(timeResolution)) {
            cal.add(Calendar.WEEK_OF_YEAR, 1);
            cal.add(Calendar.DATE, -1);
        } else if (MONTHLY.equalsIgnoreCase(timeResolution)) {
            cal.add(Calendar.MONTH, 1);
            cal.add(Calendar.DATE, -1);
        } else if (YEARLY.equalsIgnoreCase(timeResolution)) {
            cal.add(Calendar.YEAR, 1);
            cal.add(Calendar.DATE, -1);
        } else {
            //do nothing for daily currently
        }
        //we want the end date/label to not exceed the query end date
        if (maxDate != null && cal.getTime().after(maxDate)) {
            cal.setTime(maxDate);
        }
        return cal.getTime();
    }

    private GraphObject createGraph(OeDataSource dataSource, final String userPrincipalName,
            final Collection<Record> records, final Dimension dimension, final Dimension filter,
            final Dimension accumulation, DefaultGraphData data, ChartModel chart, List<Filter> filters) {

        String filterId = (filter == null) ? dimension.getId() : filter.getId();
        Map<String, String> possibleKeyValueMap = null;
        if (dimension.getPossibleValuesConfiguration() != null
                && dimension.getPossibleValuesConfiguration().getData() != null) {
            List<List<Object>> dataMap = dimension.getPossibleValuesConfiguration().getData();
            possibleKeyValueMap = new HashMap<String, String>();
            for (int i = 0; i < dataMap.size(); i++) {
                String dispVal = dataMap.get(i).size() == 2 ? dataMap.get(i).get(1).toString()
                        : dataMap.get(i).get(0).toString();
                possibleKeyValueMap.put(dataMap.get(i).get(0).toString(), dispVal);
            }
        }

        GraphDataSerializeToDiskHandler hndl = new GraphDataSerializeToDiskHandler(graphDir);
        GraphObject graph = null;

        Color[] colorsFromHex = null;
        //only set an array if they provided one
        if (!ArrayUtils.isEmpty(chart.getGraphBaseColors())) {
            colorsFromHex = ControllerUtils.getColorsFromHex(Color.BLUE, chart.getGraphBaseColors());
            //TODO when we limit the series these colors need augmented.  Create a map of id = graphbasecolor[index] first, then use that map to create a
            //new graph base color array that combines the parameter list with the default list...
            data.setGraphBaseColors(colorsFromHex);
        }

        GraphController gc = getGraphController(null, hndl, userPrincipalName);

        List<Record> recs = new ArrayList<Record>(records);

        String otherLabel = messageSource.getDataSourceMessage("graph.category.other", dataSource);

        LinkedHashMap<String, ChartData> recordMap = getRecordMap(recs, accumulation.getId(), dimension.getId(),
                filterId);
        //perform series limit
        recordMap = ControllerUtils.getSortedAndLimitedChartDataMap(recordMap, chart.getCategoryLimit(),
                otherLabel);

        //if there is no data (all zeros for a pie chart) the chart will not display anything
        if (!ControllerUtils.isCollectionValued(getCountsForChart(recordMap)) && !chart.isShowNoDataGraph()) {
            //this will hide the title and message if there is no data
            data.setGraphTitle("");
            data.setNoDataMessage("");
        }

        // Create urls for each slice/bar
        DataSeriesSource dss = null;
        StringBuilder jsCall = new StringBuilder();
        jsCall.append("javascript:OE.report.datasource.showDetails({");

        if (dataSource instanceof DataSeriesSource) {
            dss = (DataSeriesSource) dataSource;

            Collection<Dimension> dims = new ArrayList<Dimension>(dss.getResultDimensions());
            Collection<String> dimIds = ControllerUtils.getDimensionIdsFromCollection(dims);

            Collection<Dimension> accums = new ArrayList<Dimension>(dss.getAccumulations());

            for (Dimension d : accums) {
                if (dimIds.contains(d.getId()) && d.getId().equals(accumulation.getId())) {

                } else {
                    dimIds.remove(d.getId());
                }
            }

            jsCall.append("dsId:'").append(dss.getClass().getName()).append("'");
            //specify results
            jsCall.append(",results:[").append(StringUtils.collectionToDelimitedString(dimIds, ",", "'", "'"))
                    .append(']');
            //specify accumId
            jsCall.append(",accumId:'").append(accumulation.getId()).append("'");

            addJavaScriptFilters(jsCall, filters, dimension.getId());
        }

        int rSize = recordMap.size();
        int aSize = 1;
        String[] lbl = new String[rSize];
        String[][] txtb = new String[1][rSize];
        double[][] bardat = new double[aSize][rSize];
        String[][] txtp = new String[rSize][1];
        double[][] piedat = new double[rSize][aSize];
        String[][] urlsP = new String[rSize][1];
        String[][] urlsB = new String[1][rSize];
        int i = 0;
        double totalCount = 0;
        DecimalFormat df = new DecimalFormat("#.##");

        for (String key : recordMap.keySet()) {
            if (recordMap.get(key) != null && recordMap.get(key).getCount() != null
                    && !recordMap.get(key).getCount().isNaN()) {
                totalCount += recordMap.get(key).getCount();
            }
        }

        for (String key : recordMap.keySet()) {
            Double dubVal = recordMap.get(key).getCount();
            String strPercentVal = df.format(100 * dubVal / totalCount);
            lbl[i] = recordMap.get(key).getName();
            //create bar data set
            bardat[0][i] = dubVal;
            txtb[0][i] = lbl[i] + " - " + Double.toString(dubVal) + " (" + strPercentVal + "%)";
            if (lbl[i].length() > DEFAULT_LABEL_LENGTH) {
                lbl[i] = lbl[i].substring(0, DEFAULT_LABEL_LENGTH - 3) + "...";
            }
            //create pie data set
            piedat[i][0] = dubVal;
            txtp[i][0] = lbl[i] + " - " + Double.toString(dubVal) + " (" + strPercentVal + "%)";
            if (lbl[i].length() > DEFAULT_LABEL_LENGTH) {
                lbl[i] = lbl[i].substring(0, DEFAULT_LABEL_LENGTH - 3) + "...";
            }
            //TODO all "Others" to return details of all results except for those in recordMap.keyset
            //We need a "Not" filter
            if (!otherLabel.equals(key)) {
                if (dataSource instanceof DataSeriesSource) {
                    if (dimension.getId().equals(filterId) && possibleKeyValueMap != null) {
                        if (possibleKeyValueMap.containsKey(key)) {
                            urlsP[i][0] = jsCall.toString() + "," + filterId + ":'" + key + "'" + "});";
                            urlsB[0][i] = jsCall.toString() + "," + filterId + ":'" + key + "'" + "});";
                        } else {
                            urlsP[i][0] = jsCall.toString() + "});";
                            urlsB[0][i] = jsCall.toString() + "});";
                        }
                    } else {
                        if (key == null || key.equals("")
                                || key.equals(messageSource.getMessage("graph.dimension.null", "Empty Value"))) {
                            // TODO: This is when we have an ID field also marked as isResult:true and the value is null
                            // We can not provide url param filterId:null as field can be numeric and we get a java.lang.NumberFormatException...
                            urlsP[i][0] = jsCall.toString() + "});";
                            urlsB[0][i] = jsCall.toString() + "});";
                        } else {
                            urlsP[i][0] = jsCall.toString() + "," + filterId + ":'" + key + "'" + "});";
                            urlsB[0][i] = jsCall.toString() + "," + filterId + ":'" + key + "'" + "});";
                        }
                    }
                }
            }

            i++;
        }

        if (BAR.equalsIgnoreCase(chart.getType())) {
            data.setCounts(bardat);
            data.setXLabels(lbl);
            data.setMaxLabeledCategoryTicks(rSize);
            data.setAltTexts(txtb);
            if (jsCall.length() > 0) {
                data.setLineSetURLs(urlsB);
            }
            //TODO add encoding?
            graph = gc.createBarGraph(data, false, true);
        } else if (PIE.equalsIgnoreCase(chart.getType())) {
            data.setCounts(piedat);
            data.setLineSetLabels(lbl);
            data.setAltTexts(txtp);
            if (jsCall.length() > 0) {
                data.setLineSetURLs(urlsP);
            }
            graph = gc.createPieGraph(data, Encoding.PNG_WITH_TRANSPARENCY);
        }
        return graph;
    }

    private Collection<Double> getCountsForChart(LinkedHashMap<String, ChartData> recordMap) {
        Collection<Double> counts = new ArrayList<Double>();
        for (String id : recordMap.keySet()) {
            counts.add(recordMap.get(id).getCount());
        }

        return counts;
    }

    private LinkedHashMap<String, ChartData> getRecordMap(List<Record> records, String accumId, String dimensionId,
            String filterId) {
        LinkedHashMap<String, ChartData> map = new LinkedHashMap<String, ChartData>(records.size());
        int rSize = records.size();
        for (int i = 0; i < rSize; i++) {
            Record record = records.get(i);
            Object accumValue = record.getValue(accumId);
            Object dimenValue = record.getValue(dimensionId);
            Object filterValue = record.getValue(filterId == null ? dimensionId : filterId);
            String dimenString = "";
            String filterString = "";
            if (dimenValue != null) {
                dimenString = String.valueOf(dimenValue);
            } else {
                dimenString = messageSource.getMessage("graph.dimension.null");
            }
            if (filterValue != null) {
                filterString = convertFilter(filterValue);
            } else {
                filterString = messageSource.getMessage("graph.dimension.null");
            }
            try {
                Double dubVal = Double.NaN;
                if (accumValue != null) {
                    dubVal = Double.valueOf(accumValue.toString());
                }
                map.put(filterString, new ChartData(filterString, dimenString, dubVal));
            } catch (Exception e) {
                log.error("", e);
            }
        }
        return map;
    }

    @RequestMapping("/graphTimeSeries")
    public void graphTimeSeries(HttpServletRequest req, HttpServletResponse resp,
            @RequestParam("graphDataId") String dataId, @RequestParam(required = false) String graphTitle,
            @RequestParam(required = false) String xAxisLabel,
            // TODO put these all in a graph model object and let Spring deserialize from JSON
            @RequestParam(required = false) String yAxisLabel, @RequestParam(required = false) Double yAxisMin,
            @RequestParam(required = false) Double yAxisMax, @RequestParam(required = false) String dataDisplayKey,
            @RequestParam(required = false) String getImageMap, @RequestParam(required = false) String imageType,
            @RequestParam(required = false) String resolution,
            @RequestParam(required = false) String getHighResFile) throws GraphException, IOException {

        GraphDataSerializeToDiskHandler hndl = new GraphDataSerializeToDiskHandler(graphDir);
        GraphController gc = getGraphController(dataId, hndl, req.getUserPrincipal().getName());

        GraphDataInterface data = hndl.getGraphData(dataId);

        if (graphTitle != null) {
            data.setGraphTitle(graphTitle);
        }
        if (xAxisLabel != null) {
            data.setXAxisLabel(xAxisLabel);
        }
        if (yAxisLabel != null) {
            data.setYAxisLabel(yAxisLabel);
        }

        GraphObject graph = gc.createTimeSeriesGraph(data, yAxisMin, yAxisMax, dataDisplayKey);
        BufferedOutputStream out = new BufferedOutputStream(resp.getOutputStream());

        if (getImageMap != null && (getImageMap.equals("1") || getImageMap.equalsIgnoreCase("true"))) {
            resp.setContentType("text/plain;charset=utf-8");
            StringBuffer sb = new StringBuffer();
            sb.append(graph.getImageMap());
            out.write(sb.toString().getBytes());
        } else {
            resp.setContentType("image/png;charset=utf-8");
            String filename = graph.getImageFileName();
            filename = filename.replaceAll("\\s", "_");
            resp.setHeader("Content-disposition", "attachment; filename=" + filename);
            int imageResolution = 300;
            if (resolution != null) {
                try {
                    imageResolution = Integer.parseInt(resolution);
                    graph.writeChartAsHighResolutionPNG(out, data.getGraphWidth(), data.getGraphHeight(),
                            imageResolution);
                } catch (Exception e) {
                    log.error("", e);
                }
            } else {
                graph.writeChartAsPNG(out, data.getGraphWidth(), data.getGraphHeight());
            }
        }
    }

    @RequestMapping("/graphBar")
    public void graphBar(HttpServletRequest req, HttpServletResponse resp,
            @RequestParam("graphDataId") String dataId, @RequestParam(required = false) Integer resolution)
            throws GraphException, IOException {
        GraphDataSerializeToDiskHandler hndl = new GraphDataSerializeToDiskHandler(graphDir);
        GraphController gc = getGraphController(dataId, hndl, req.getUserPrincipal().getName());

        GraphDataInterface data = hndl.getGraphData(dataId);
        GraphObject graph = gc.createBarGraph(data, false);
        String filename = graph.getImageFileName();
        filename = filename.replaceAll("\\s", "_");
        resp.setContentType("image/png;charset=utf-8");
        resp.setHeader("Content-disposition", "attachment; filename=" + filename);

        OutputStream out = resp.getOutputStream();
        // why can't the graph module handle this?
        if (resolution == null) {
            graph.writeChartAsPNG(out, data.getGraphWidth(), data.getGraphHeight());
        } else {
            graph.writeChartAsHighResolutionPNG(out, data.getGraphWidth(), data.getGraphHeight(), resolution);
        }

    }

    @RequestMapping("/graphPie")
    public void graphPie(HttpServletRequest req, HttpServletResponse resp,
            @RequestParam("graphDataId") String dataId, @RequestParam(required = false) Integer resolution)
            throws GraphException, IOException {

        GraphDataSerializeToDiskHandler hndl = new GraphDataSerializeToDiskHandler(graphDir);
        GraphController gc = getGraphController(dataId, hndl, req.getUserPrincipal().getName());

        GraphDataInterface data = hndl.getGraphData(dataId);
        GraphObject graph = gc.createPieGraph(data);
        String filename = graph.getImageFileName();
        filename = filename.replaceAll("\\s", "_");
        resp.setContentType("image/png;charset=utf-8");
        resp.setHeader("Content-disposition", "attachment; filename=" + filename);

        OutputStream out = resp.getOutputStream();
        // why can't the graph module handle this?
        if (resolution == null) {
            graph.writeChartAsPNG(out, data.getGraphWidth(), data.getGraphHeight());
        } else {
            graph.writeChartAsHighResolutionPNG(out, data.getGraphWidth(), data.getGraphHeight(), resolution);
        }
    }

    @RequestMapping("/detailsQuery")
    public @ResponseBody DataSourceDetails detailsQuery(WebRequest request,
            @RequestParam("dsId") JdbcOeDataSource ds,
            @RequestParam(value = "firstrecord", defaultValue = "0") long firstRecord,
            @RequestParam(value = "pagesize", defaultValue = "200") long pageSize)
            throws ErrorMessageException, OeDataSourceException, OeDataSourceAccessException {

        List<Filter> filters = new Filters().getFilters(request.getParameterMap(), ds, null, 0, null, 0);
        List<Dimension> results = ControllerUtils.getResultDimensionsByIds(ds,
                request.getParameterValues("results"));
        List<Dimension> accumulations = ControllerUtils.getAccumulationsByIds(ds,
                request.getParameterValues("accumId"));

        final List<OrderByFilter> sorts = new ArrayList<OrderByFilter>();
        try {
            sorts.addAll(Sorters.getSorters(request.getParameterMap()));
        } catch (Exception e) {
            log.warn("Unable to get sorters, using default ordering");
        }

        String clientTimezone = null;
        String timezoneEnabledString = messageSource.getMessage(TIMEZONE_ENABLED, "false");
        if (timezoneEnabledString.equalsIgnoreCase("true")) {
            clientTimezone = ControllerUtils.getRequestTimezoneAsHourMinuteString(request);
        }
        return new DetailsQuery().performDetailsQuery(ds, results, accumulations, filters, sorts, false,
                clientTimezone, firstRecord, pageSize, true);
    }

    @RequestMapping("/detailsPivot")
    public @ResponseBody DataSourceDetails detailsPivot(WebRequest request,
            @RequestParam("dsId") JdbcOeDataSource ds, @RequestParam(value = "pivotX") String pivotX,
            @RequestParam(value = "pivotY") String pivotY,
            @RequestParam(value = "firstrecord", defaultValue = "0") long firstRecord,
            @RequestParam(value = "pagesize", defaultValue = "200") long pageSize)
            throws ErrorMessageException, OeDataSourceException, OeDataSourceAccessException {

        // Do the normal details query using the pivots as dimensions
        DataSourceDetails details = this.detailsQuery(request, ds, firstRecord, pageSize);

        // Expand the sparse results into a full matrix
        Map<String, Map<String, Integer>> matrix = new LinkedHashMap<String, Map<String, Integer>>();
        matrix.put("Total", new LinkedHashMap<String, Integer>());
        // TODO: Colin: (Round 2 feature) Perform some check to disable the pivot button if the report doesn't have an ACCUM field, or if it only has 1 "DS" field
        // TODO: Colin: (Round 2 feature) Put this where the button gets created in a JS file.
        String accum = null;
        for (String key : details.getRows().get(0).keySet()) {
            if (!key.equals(pivotX) && !key.equals(pivotY)) {
                accum = key;
                break;
            }
        }

        for (Map<String, Object> row : details.getRows()) {
            // Add a new column to the matrix for the value of the pivotX dimension if needed
            // Otherwise, find the column for the value of the pivotX dimension
            Map<String, Integer> col;
            if (!matrix.containsKey(String.valueOf(row.get(pivotX)))) {
                col = (matrix.size() < 1) ? new LinkedHashMap<String, Integer>()
                        : new LinkedHashMap<String, Integer>(matrix.get(matrix.keySet().iterator().next()));
                for (Map.Entry<String, Integer> e : col.entrySet()) {
                    col.put(e.getKey(), null);
                }
                matrix.put(String.valueOf(row.get(pivotX)), col);
            } else {
                col = matrix.get(String.valueOf(row.get(pivotX)));
            }

            // Make sure that every column in the matrix has a key for the value of the pivotY dimension
            // If not, set it to 0
            for (Map.Entry<String, Map<String, Integer>> e : matrix.entrySet()) {
                if (!e.getValue().containsKey(String.valueOf(row.get(pivotY)))) {
                    e.getValue().put(String.valueOf(row.get(pivotY)), null);
                }
            }

            // Set the pivotX column at pivotY as the value of the accumulation
            col.put(String.valueOf(row.get(pivotY)), (Integer) row.get(accum));

            // Add the accumulation value to the total matrix for bookkeeping
            Integer oldValue = matrix.get("Total").get(String.valueOf(row.get(pivotY)));
            oldValue = (oldValue != null) ? oldValue : 0;
            matrix.get("Total").put(String.valueOf(row.get(pivotY)), oldValue + (Integer) row.get(accum));
        }

        // Calculate totals for each column
        for (Map.Entry<String, Map<String, Integer>> row : matrix.entrySet()) {
            Integer total = 0;
            for (Map.Entry<String, Integer> col : row.getValue().entrySet()) {
                if (col.getValue() != null) {
                    total += col.getValue();
                }
            }
            row.getValue().put("Total", total);
        }

        // Move the total column to the end
        matrix.put("Total", matrix.remove("Total"));

        // Reformat the expanded data back into rows for the grid
        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
        for (String key : matrix.get(matrix.keySet().iterator().next()).keySet()) {
            Map<String, Object> row = new LinkedHashMap<String, Object>();
            row.put(pivotY, key);
            for (String col : matrix.keySet()) {
                row.put(col, matrix.get(col).get(key));
            }
            rows.add(row);
        }

        DataSourceDetails pivotDetails = new DataSourceDetails();
        pivotDetails.setRows(rows);
        pivotDetails.setTotalRecords(rows.size());
        return pivotDetails;
    }

    private int getCalWeekStartDay(Map<String, ResolutionHandler> resolutionHandlers) {
        ResolutionHandler handler = resolutionHandlers.get("weekly");
        int startDay;
        if (handler == null || !(handler instanceof PgSqlWeeklyHandler)) {
            switch (Integer.parseInt(messageSource.getMessage("epidemiological.day.start",
                    Integer.toString(DEFAULT_WEEK_STARTDAY)))) {
            case 0:
                startDay = Calendar.SUNDAY;
                break;
            case 2:
                startDay = Calendar.TUESDAY;
                break;
            case 3:
                startDay = Calendar.WEDNESDAY;
                break;
            case 4:
                startDay = Calendar.THURSDAY;
                break;
            case 5:
                startDay = Calendar.FRIDAY;
                break;
            case 6:
                startDay = Calendar.SATURDAY;
                break;
            default:
                startDay = Calendar.MONDAY;
                break;
            }
            return startDay;
        }
        return ((PgSqlWeeklyHandler) handler).getCalWeekStartDay();
    }

    private int getWeekStartDay(Map<String, ResolutionHandler> resolutionHandlers) {
        ResolutionHandler handler = resolutionHandlers.get("weekly");
        if (handler == null || !(handler instanceof PgSqlWeeklyHandler)) {
            return Integer.parseInt(
                    messageSource.getMessage("epidemiological.day.start", Integer.toString(DEFAULT_WEEK_STARTDAY)));
        }
        return ((PgSqlWeeklyHandler) handler).getWeekStartDay();
    }

    /**
     * Extracts AccumPoint from a Collection of <code>records</code> where
     *
     * @param principal, used for logging
     */
    private List<AccumPoint> extractAccumulationPoints(String principal, DataSeriesSource ds,
            final Collection<Record> records, Date startDate, Date endDate, List<Dimension> accumulations,
            final GroupingImpl group, Map<String, ResolutionHandler> resolutionHandlers) {
        log.info(LogStatements.TIME_SERIES.getLoggingStmt() + principal);
        int startDayCal = getCalWeekStartDay(resolutionHandlers);
        int startDay = getWeekStartDay(resolutionHandlers);

        String resolution = group.getResolution();
        final String groupId = group.getId();
        int zeroFillInterval = intervalMap.keySet().contains(resolution) ? intervalMap.get(resolution) : -1;

        GroupingDimension grpdim = ds.getGroupingDimension(group.getId());
        if (zeroFillInterval != -1
                && (grpdim.getSqlType() == FieldType.DATE || grpdim.getSqlType() == FieldType.DATE_TIME)) {
            ArrayList<AccumPoint> fullVector = new ArrayList<AccumPoint>(records.size());
            //create zero points for each accumulation
            Map<String, Number> zeroes = new HashMap<String, Number>();
            for (Dimension accumulation : accumulations) {
                zeroes.put(accumulation.getId(), 0);
            }
            if (records.size() > 0) {
                final Calendar cal = new GregorianCalendar();
                cal.setTime(startDate);
                Iterator<Record> recordsIterator = records.iterator();
                Record currRecord = (recordsIterator.hasNext()) ? recordsIterator.next() : null;

                //currently iterates over data incrementing cal by the resolution (weekly, daily etc)
                for (int i = 1; !cal.getTime().after(endDate); i++) {
                    // if (DAILY.equalsIgnoreCase(resolution)) {
                    boolean addRecord = false;
                    if (currRecord != null) {
                        // 2013/03/25, SCC, GGF, There are some weird edge cases with selecting data ranges that span
                        // the EDT/EST cross-overs.  Sometimes the "filter" will have the "23:00" and the data will have
                        // "00:00".  So, since we only care about the "date" anyway, clear out any subordinate fields.

                        // Database record date (set hour, minute, second, millisec to 0)
                        final Calendar rowValue = Calendar.getInstance();
                        rowValue.setTime((Date) currRecord.getValue(groupId));
                        rowValue.set(Calendar.HOUR_OF_DAY, 0);
                        rowValue.set(Calendar.MINUTE, 0);
                        rowValue.set(Calendar.SECOND, 0);
                        rowValue.set(Calendar.MILLISECOND, 0);

                        // looping variable date (set hour, minute, second, millisec to 0)
                        final Calendar calValue = Calendar.getInstance();
                        calValue.setTime(cal.getTime());
                        calValue.set(Calendar.HOUR_OF_DAY, 0);
                        calValue.set(Calendar.MINUTE, 0);
                        calValue.set(Calendar.SECOND, 0);
                        calValue.set(Calendar.MILLISECOND, 0);

                        if (resolution.equalsIgnoreCase(DAILY) && rowValue.equals(calValue)) {
                            addRecord = true;
                        } else {
                            Calendar currRecCalendar = new GregorianCalendar();
                            currRecCalendar.setTime((Date) currRecord.getValue(groupId));
                            if (resolution.equalsIgnoreCase(WEEKLY)) {
                                if (PgSqlDateHelper.getYear(startDay, cal) == PgSqlDateHelper.getYear(startDay,
                                        currRecCalendar)
                                        && PgSqlDateHelper.getWeekOfYear(startDay, cal) == PgSqlDateHelper
                                                .getWeekOfYear(startDay, currRecCalendar)) {
                                    addRecord = true;
                                }
                            } else if (resolution.equalsIgnoreCase(MONTHLY)) {
                                if (cal.get(Calendar.YEAR) == currRecCalendar.get(Calendar.YEAR)
                                        && cal.get(Calendar.MONTH) == currRecCalendar.get(Calendar.MONTH)) {
                                    addRecord = true;
                                }
                            }
                        }
                        if (addRecord) {
                            //if the current record matches the date put it in
                            fullVector.add(createAccumulationPoint(currRecord, accumulations));
                            currRecord = (recordsIterator.hasNext()) ? recordsIterator.next() : null;
                        }
                    }
                    if (!addRecord) {
                        //add a zero fill
                        Map<String, Dimension> m = new HashMap<String, Dimension>();
                        m.put(groupId, ds.getResultDimension(groupId));
                        HashMap<String, Object> map = new HashMap<String, Object>();
                        map.put(groupId, cal.getTime());
                        Record r = new QueryRecord(m, map);
                        fullVector.add(new AccumPointImpl(zeroes, r));
                    }
                    if (resolution.equalsIgnoreCase(WEEKLY)) {
                        // add 7 days if current date falls on week start date
                        if (i != 1) {
                            cal.add(Calendar.WEEK_OF_YEAR, 1);
                        } else {
                            cal.add(Calendar.DAY_OF_YEAR, 1);
                            while (cal.get(Calendar.DAY_OF_WEEK) != startDayCal) {
                                cal.add(Calendar.DAY_OF_YEAR, 1);
                            }
                        }
                    } else {
                        // reset the date each time to account for +month oddness
                        cal.setTime(startDate);
                        // increment the interval
                        cal.add(zeroFillInterval, 1 * i);
                    }
                }

            }
            return fullVector;
        } else {
            //pretty sure this is raw non filled
            List<AccumPoint> rawVector = new ArrayList<AccumPoint>(records.size());
            for (Record record : records) {
                rawVector.add(createAccumulationPoint(record, accumulations));
            }
            return rawVector;
        }
    }

    private AccumPoint createAccumulationPoint(Record record, List<Dimension> accumulations) {
        Map<String, Number> values = new LinkedHashMap<String, Number>();
        for (Dimension a : accumulations) {
            Object value = record.getValue(a.getId());
            Number number = (Number) value;
            values.put(a.getId(), number);
        }
        AccumPoint accumPoint = new AccumPointImpl(values, record);
        return accumPoint;
    }

    /**
     * Takes a List of SeriesPoints and generates a double array that holds the value of each SeriesPoint
     *
     * @param seriespoints - List<AccumPoint> whose values need to be extracted into a double[] for detectors
     * @param dimId        - The dimension id to pull from each AccumPoint
     * @return pointarray - double[] that holds all the values from the passed in list of SeriesPoint
     */
    private double[] generateSeriesValues(List<AccumPoint> seriespoints, String dimId) {
        //List<Number> pointlist = new ArrayList<Number>();
        double[] pointarray = new double[seriespoints.size()];
        int i = 0;
        for (AccumPoint point : seriespoints) {
            //pointlist.add(point.getValue());
            if (point != null && point.getValue(dimId) != null) {
                pointarray[i] = point.getValue(dimId).doubleValue();
            } else {
                pointarray[i] = Double.NaN;
            }
            i++;
        }
        return pointarray;
    }

    /**
     * Takes a List of AccumPoints and generates a double array that holds the total of accumulations
     *
     * @param points     - List<AccumPoint> whose values need to be extracted into a double[] for detectors
     * @param dimensions - The list of dimensions to sum from each AccumPoint
     * @return double[] that holds all the values from the passed in list of SeriesPoint
     */
    private double[] totalSeriesValues(List<AccumPoint> points, List<Dimension> dimensions) {
        double[] totalArray = new double[points.size()];
        int i = 0;
        for (AccumPoint point : points) {
            if (point != null) {
                for (Dimension dim : dimensions) {
                    Number value = point.getValue(dim.getId());
                    if (value != null) {
                        totalArray[i] = totalArray[i] + value.doubleValue();
                    }
                }
            } else {
                totalArray[i] = Double.NaN;
            }
            i++;
        }
        return totalArray;
    }

    /**
     * Returns a File Download Dialog for a file containing information in the data details grid.
     *
     * @param request  the request contains needed parameters: the 'results' headers that appear in the grid
     * @param response the response object for this request
     */
    @RequestMapping("/exportGridToFile")
    public void exportGridToFile(@RequestParam("dsId") JdbcOeDataSource ds, ServletWebRequest request,
            HttpServletResponse response) throws ErrorMessageException, IOException {

        TimeZone timezone = ControllerUtils.getRequestTimezone(request);
        response.setContentType("application/json;charset=utf-8");

        final List<Filter> filters = new Filters().getFilters(request.getParameterMap(), ds, null, 0, null, 0);
        final List<Dimension> results = ControllerUtils.getResultDimensionsByIds(ds,
                request.getParameterValues("results"));

        final List<String> columnHeaders = new ArrayList<String>();
        for (final Dimension result : results) {
            if (result.getDisplayName() != null) {
                columnHeaders.add(result.getDisplayName());
            } else {
                columnHeaders.add(messageSource.getDataSourceMessage(result.getId(), ds));
            }
        }

        final List<Dimension> accumulations = ControllerUtils.getAccumulationsByIds(ds,
                request.getParameterValues("accumId"));

        final List<OrderByFilter> sorts = new ArrayList<OrderByFilter>();
        try {
            sorts.addAll(Sorters.getSorters(request.getParameterMap()));
        } catch (Exception e) {
            log.warn("Unable to get sorters, using default ordering");
        }

        String clientTimezone = null;
        String timezoneEnabledString = messageSource.getMessage(TIMEZONE_ENABLED, "false");
        if (timezoneEnabledString.equalsIgnoreCase("true")) {
            clientTimezone = ControllerUtils.getRequestTimezoneAsHourMinuteString(request);
        }
        Collection<Record> points = new DetailsQuery().performDetailsQuery(ds, results, accumulations, filters,
                sorts, false, clientTimezone);
        response.setContentType("text/csv;charset=utf-8");

        String filename = messageSource.getDataSourceMessage("panel.details.export.file", ds) + "-"
                + new DateTime().toString("yyyyMMdd'T'HHmmss") + ".csv";
        response.setHeader("Content-disposition", "attachment; filename=" + filename);

        // Cache-Control = cache and Pragma = cache enable IE to download files over SSL.
        response.setHeader("Cache-Control", "cache");
        response.setHeader("Pragma", "cache");
        FileExportUtil.exportGridToCSV(response.getWriter(),
                columnHeaders.toArray(new String[columnHeaders.size()]), points, timezone);
    }

    private String appendUrlParameter(String url, String param, String value) {
        StringBuilder sb = new StringBuilder(url);
        if (url.contains("?")) {
            sb.append('&');
        } else {
            sb.append('?');
        }

        URLCodec codec = new URLCodec();

        try {
            sb.append(codec.encode(param));
        } catch (EncoderException e) {
            log.error("Exception encoding URL param " + param, e);
        }

        try {
            sb.append('=').append(codec.encode(value));
        } catch (EncoderException e) {
            log.error("Exception encoding URL value " + value, e);
        }

        return sb.toString();
    }

    /**
     * If the graph.font property is specified, append its value as a parameter to the given URL. Otherwise, do
     * nothing.
     *
     * @return new URL
     */
    private String appendGraphFontParam(JdbcOeDataSource dataSource, String url) {
        try {
            String graphFont = messageSource.getDataSourceMessage("graph.font", dataSource);
            return appendUrlParameter(url, "font", graphFont);
        } catch (NoSuchMessageException e) {
            log.debug("Property graph.font not found, using default");
            return url;
        }
    }

    /**
     * Get a new GraphContoller instance with sane metadata
     */
    private GraphController getGraphController(String graphDataId, GraphDataHandlerInterface graphDataHandler,
            String userId) {
        final String graphFont = messageSource.getMessage("graph.font", "Arial");

        GraphController graphController = new GraphController(graphDataId, graphDataHandler, userId) {

            // TODO graph module needs to be totally rewritten
            private void setGraphMetaData(Map<String, Object> graphMetaData) {
                graphMetaData.put(GraphSource.GRAPH_FONT, new Font(graphFont, Font.BOLD, 14));
                graphMetaData.put(GraphSource.GRAPH_Y_AXIS_FONT, new Font(graphFont, Font.BOLD, 12));
                graphMetaData.put(GraphSource.GRAPH_Y_AXIS_LABEL_FONT, new Font(graphFont, Font.BOLD, 12));
                graphMetaData.put(GraphSource.GRAPH_X_AXIS_FONT, new Font(graphFont, Font.PLAIN, 11));
                graphMetaData.put(GraphSource.GRAPH_X_AXIS_LABEL_FONT, new Font(graphFont, Font.PLAIN, 12));
                graphMetaData.put(GraphSource.LEGEND_FONT, new Font(graphFont, Font.PLAIN, 12));
            }

            @Override
            public void setTimeSeriesGraphMetaData(GraphDataInterface graphData, Double yAxisMin, Double yAxisMax,
                    double maxCount, Map<String, Object> graphMetaData) {

                super.setTimeSeriesGraphMetaData(graphData, yAxisMin, yAxisMax, maxCount, graphMetaData);
                setGraphMetaData(graphMetaData);
            }

            @Override
            public void setBarGraphMetaData(GraphDataInterface graphData, boolean stackGraph,
                    Map<String, Object> graphMetaData) {

                super.setBarGraphMetaData(graphData, stackGraph, graphMetaData);
                setGraphMetaData(graphMetaData);
            }

            @Override
            public void setPieGraphMetaData(GraphDataInterface graphData, Map<String, Object> graphMetaData,
                    List<PointInterface> points) {
                super.setPieGraphMetaData(graphData, graphMetaData, points);
                setGraphMetaData(graphMetaData);
            }
        };
        Map<String, String> translationMap = graphController.getTranslationMap();
        translationMap.put("Normal", messageSource.getMessage("graph.normal"));
        translationMap.put("Warning", messageSource.getMessage("graph.warning"));
        translationMap.put("Alert", messageSource.getMessage("graph.alert"));
        graphController.setTranslationMap(translationMap);

        return graphController;
    }

    /**
     * Used to add all of the given filters to the JavaScript callback.  This is intended to fix the issue when the user
     * selects more than 1 item in a multi-select filter box.  It will also handle converting dates into their numeric
     * format for later parsing on the backend.
     *
     * @param javaScript   The string builder that contains the JavaScript callback information.
     * @param filters      The filters to add.
     * @param ignoredField Sometimes, we want to ignore adding a certain filter to the callback.
     */
    private static void addJavaScriptFilters(final StringBuilder javaScript, final Collection<Filter> filters,
            final String ignoredField) {
        if ((filters != null) && (!filters.isEmpty())) {
            for (final Filter filter : filters) {
                if ((filter != null) && (filter instanceof FieldFilter)) {
                    final FieldFilter fieldFilter = (FieldFilter) filter;
                    final String id = fieldFilter.getFilterId();
                    final List<Object> arguments = fieldFilter.getArguments();

                    if ((id != null) && (!id.equals(ignoredField)) && (arguments != null)
                            && (!arguments.isEmpty())) {
                        if (arguments.size() == 1) {
                            final Object value = arguments.get(0);
                            if (fieldFilter instanceof LteqFilter) {
                                javaScript.append(",").append(id).append("_end:'").append(convertFilter(value))
                                        .append("'");
                            } else if (fieldFilter instanceof GteqFilter) {
                                javaScript.append(",").append(id).append("_start:'").append(convertFilter(value))
                                        .append("'");
                            } else {
                                javaScript.append(",").append(id).append(":'").append(convertFilter(value))
                                        .append("'");
                            }
                        } else {
                            javaScript.append(",").append(id).append(":[");
                            for (int loopIndex = 0; loopIndex < arguments.size(); loopIndex++) {
                                javaScript.append(loopIndex == 0 ? "" : ",").append("'")
                                        .append(convertFilter(arguments.get(loopIndex))).append("'");
                            }
                            javaScript.append("]");
                        }
                    }
                }
            }
        }
    }

    /**
     * Used to convert the given dimension value into a Javascript-safe function call value.
     *
     * <p> In the event that the object is a <code>java.util.Date</code>, the numeric will be returned.
     *
     * <p> If <code>null</code> is given, then an empty string is returned.
     *
     * @param value The dimension value to be converted.
     * @return The safe string representation of the given value.
     */
    private static String convertFilter(final Object value) {
        // http://en.wikipedia.org/wiki/Percent-encoding
        //     All Reserved Chars:    ! # $ & ' ( ) * + , / : ; = ? @ [ ]
        //     Others Handled:        % < > ` ~ ^ | { } . - " \ _

        // If a literal _ or % (the single char and variable char wild card symbols in PostgreSQL)
        // are desired in the filter criteria, then they need to be backslash escaped by the user.

        // Yes, some need lots of backslashes due to various levels of decoding between PostgreSQL, Java, Javascript

        if (value == null) {
            return "";
        } else if (value instanceof Date) {
            return String.valueOf(((Date) value).getTime());
        } else {
            return String.valueOf(value).replaceAll("%", "%25") // To fix query filters: %fever%
                    .replaceAll("\\\\", "\\\\\\\\") // To fix chart groupings: This is a sad face :\
                    .replaceAll("'", "\\\\'") // To fix chart groupings: Prince George's
                    .replaceAll("\"", "&quot;") // To fix chart groupings: Bob said "his quote".
                    .replaceAll(" ", "%20");
        }
    }
}