ddf.metrics.reporting.internal.rest.MetricsEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for ddf.metrics.reporting.internal.rest.MetricsEndpoint.java

Source

/**
 * Copyright (c) Codice Foundation
 * <p>
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License
 * is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package ddf.metrics.reporting.internal.rest;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.configuration.SystemBaseUrl;
import org.codice.ddf.configuration.SystemInfo;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.json.simple.JSONValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ddf.metrics.reporting.internal.MetricsEndpointException;
import ddf.metrics.reporting.internal.MetricsGraphException;
import ddf.metrics.reporting.internal.MetricsRetriever;
import ddf.metrics.reporting.internal.rrd4j.RrdMetricsRetriever;

/**
 * This class provides an endpoint for a client, e.g., {@code MetricsWebConsolePlugin} to access the
 * historical metrics data collected by DDF.
 * <p>
 * This endpoint provides a URL to retrieve the list of metrics collected by DDF, including their
 * associated URLs to access pre-defined time ranges of each metric's historical data, e.g., for the
 * past 15 minutes, 1 hour, 4 hours, 12 hours, 24 hours, 3 days, 1 week, 1 month, and 1 year. Each
 * of these hyperlinks will return a byte array containing a PNG graph of the metric's historical
 * data for the given time range.
 */
@Path("/")
public class MetricsEndpoint {
    public static final String DEFAULT_METRICS_DIR = "data/metrics/";

    static final Map<String, Long> TIME_RANGES = new HashMap<String, Long>();

    private static final Logger LOGGER = LoggerFactory.getLogger(MetricsEndpoint.class);

    private static final String METRICS_SERVICE_BASE_URL = "/internal/metrics";

    private static final String RRD_FILE_EXTENSION = ".rrd";

    private static final String JSON_MIME_TYPE = "application/json";

    private static final String PNG_MIME_TYPE = "image/png";

    private static final String START_DATE_QUERY = "?startDate=";

    private static final String END_DATE_QUERY = "&endDate=";

    private static final String DATE_OFFSET_QUERY = "?dateOffset=";

    private static final int MILLISECONDS_PER_SECOND = 1000;

    private static final long FIFTEEN_MINUTES_IN_SECONDS = 15 * 60;

    private static final long ONE_HOUR_IN_SECONDS = 4 * FIFTEEN_MINUTES_IN_SECONDS;

    private static final long ONE_DAY_IN_SECONDS = 24 * ONE_HOUR_IN_SECONDS;

    private static final long ONE_WEEK_IN_SECONDS = 7 * ONE_DAY_IN_SECONDS;

    private static final long ONE_MONTH_IN_SECONDS = 30 * ONE_DAY_IN_SECONDS;

    private static final long THREE_MONTHS_IN_SECONDS = 90 * ONE_DAY_IN_SECONDS;

    private static final long SIX_MONTHS_IN_SECONDS = 180 * ONE_DAY_IN_SECONDS;

    private static final long ONE_YEAR_IN_SECONDS = 365 * ONE_DAY_IN_SECONDS;

    private static final String PNG_FORMAT = "png";

    private static final String CSV_FORMAT = "csv";

    static {
        TIME_RANGES.put("15m", FIFTEEN_MINUTES_IN_SECONDS);
        TIME_RANGES.put("1h", ONE_HOUR_IN_SECONDS);
        TIME_RANGES.put("1d", ONE_DAY_IN_SECONDS);
        TIME_RANGES.put("1w", ONE_WEEK_IN_SECONDS);
        TIME_RANGES.put("1M", ONE_MONTH_IN_SECONDS);
        TIME_RANGES.put("3M", THREE_MONTHS_IN_SECONDS);
        TIME_RANGES.put("6M", SIX_MONTHS_IN_SECONDS);
        TIME_RANGES.put("1y", ONE_YEAR_IN_SECONDS);
    }

    private final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

    private String metricsDir = DEFAULT_METRICS_DIR;

    private MetricsRetriever metricsRetriever = new RrdMetricsRetriever();

    private double metricsMaxThreshold;

    /**
     * Retrieve data for the specified metric over the given time range. The URL to access this
     * method is of the form http://<host>:<port>/<metricName>.<outputFormat> So the desired metric
     * filename is specified in the URL, e.g., catalogQueryCount.png, where the filename extension
     * defines the desired output format returned for the metric's data. Currently supported formats
     * are png, csv, xls, ppt, xml, and json.
     * <p>
     * Note that the time range can be specified as either a start and end date (in RFC3339 format,
     * i.e., YYYY-MM-DD'T'hh:mm:ssZ), or as an offset in seconds from the current time. These 2 time
     * range mechanisms cannot be combined, e.g., you cannot specify an end date and an offset to be
     * applied from that end date.
     * <p>
     * By default, the metric's name will be used for the y-axis label on the PNG graph, and the
     * metric name and time range will be used for the graph's title. Both of these can be
     * optionally specified with the yAxisLabel and title parameters. These 2 parameters do not
     * apply for the other formats.
     *
     * @param metricName   Name of the metric being graphed, e.g., queryCount
     * @param outputFormat output format of the metric, e.g. csv
     * @param startDate    Specifies the start of the time range of the search on the metric's data (RFC-3339
     *                     - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Cannot be used with dateOffset
     *                     parameter.
     * @param endDate      Specifies the end of the time range of the search on the metric's data (RFC-3339 -
     *                     Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Cannot be used with dateOffset
     *                     parameter.
     * @param dateOffset   Specifies an offset, backwards from the current time, to search on the modified
     *                     time field for entries. Defined in seconds. Cannot be used with startDate and
     *                     endDate parameters.
     * @param yAxisLabel   (optional) the label to apply to the graph's y-axis
     * @param title        (optional) the title to be applied to the graph
     * @param uriInfo
     * @return Response containing the metric's data in the specified outputFormat
     * @throws MetricsEndpointException
     */
    @GET
    @Path("/{metricName}.{outputFormat}")
    public Response getMetricsData(@PathParam("metricName") String metricName,
            @PathParam("outputFormat") String outputFormat, @QueryParam("startDate") String startDate,
            @QueryParam("endDate") String endDate, @QueryParam("dateOffset") String dateOffset,
            @QueryParam("yAxisLabel") String yAxisLabel, @QueryParam("title") String title,
            @Context UriInfo uriInfo) throws MetricsEndpointException {
        LOGGER.trace(
                "ENTERING: getMetricsData  -  metricName = " + metricName + ",    outputFormat = " + outputFormat);
        LOGGER.trace("request url: " + uriInfo.getRequestUri());
        LOGGER.trace("startDate = " + startDate + ",     endDate = " + endDate);
        LOGGER.trace("dateOffset = " + dateOffset);

        Response response = null;

        // Client must specify *either* startDate and/or endDate *OR* dateOffset
        if (!StringUtils.isBlank(dateOffset)
                && (!StringUtils.isBlank(startDate) || !StringUtils.isBlank(endDate))) {
            throw new MetricsEndpointException(
                    "Cannot specify dateOffset and startDate or endDate, must specify either dateOffset only or startDate and/or endDate",
                    Response.Status.BAD_REQUEST);
        }

        long endTime;
        if (!StringUtils.isBlank(endDate)) {
            endTime = parseDate(endDate);
            LOGGER.trace("Parsed endTime = " + endTime);
        } else {
            // Default end time for metrics graphing to now (in seconds)
            Calendar now = getCalendar();
            endTime = now.getTimeInMillis() / MILLISECONDS_PER_SECOND;
            LOGGER.trace("Defaulted endTime to " + endTime);

            // Set endDate to new calculated endTime (so that endDate is displayed properly
            // in graph's title)
            endDate = dateFormatter.format(now.getTime());
        }

        long startTime;
        if (!StringUtils.isBlank(startDate)) {
            startTime = parseDate(startDate);
            LOGGER.trace("Parsed startTime = " + startTime);
        } else if (!StringUtils.isBlank(dateOffset)) {
            startTime = endTime - Long.parseLong(dateOffset);
            LOGGER.trace("Offset-computed startTime = " + startTime);

            // Set startDate to new calculated startTime (so that startDate is displayed properly
            // in graph's title)
            Calendar cal = getCalendar();
            cal.setTimeInMillis(startTime * MILLISECONDS_PER_SECOND);
            startDate = dateFormatter.format(cal.getTime());
        } else {
            // Default start time for metrics graphing to end time last 24 hours (in seconds)
            startTime = endTime - ONE_DAY_IN_SECONDS;
            LOGGER.trace("Defaulted startTime to " + startTime);

            // Set startDate to new calculated startTime (so that startDate is displayed properly
            // in graph's title)
            Calendar cal = getCalendar();
            cal.setTimeInMillis(startTime * MILLISECONDS_PER_SECOND);
            startDate = dateFormatter.format(cal.getTime());
        }

        LOGGER.trace("startDate = " + startDate + ",   endDate = " + endDate);

        if (StringUtils.isBlank(yAxisLabel)) {
            yAxisLabel = RrdMetricsRetriever.convertCamelCase(metricName);
        }

        if (StringUtils.isBlank(title)) {
            title = RrdMetricsRetriever.convertCamelCase(metricName) + " for " + startDate + " to " + endDate;
        }

        // Convert metric filename to rrd filename (because RRD file required by MetricRetriever to
        // generate graph)
        String rrdFilename = metricsDir + metricName + RRD_FILE_EXTENSION;

        if (outputFormat.equalsIgnoreCase(PNG_FORMAT)) {
            LOGGER.trace("Retrieving PNG-formatted data for metric " + metricName);
            try {
                byte[] metricsGraphBytes = metricsRetriever.createGraph(metricName, rrdFilename, startTime, endTime,
                        yAxisLabel, title);
                ByteArrayInputStream bis = new ByteArrayInputStream(metricsGraphBytes);
                response = Response.ok(bis, PNG_MIME_TYPE).build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create graph for metric " + metricName);
                throw new MetricsEndpointException("Cannot create metrics graph for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        } else if (outputFormat.equalsIgnoreCase("csv")) {
            try {
                String csv = metricsRetriever.createCsvData(rrdFilename, startTime, endTime);
                ResponseBuilder responseBuilder = Response.ok(csv);
                responseBuilder.type("text/csv");
                response = responseBuilder.build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create CSV data for metric " + metricName);
                throw new MetricsEndpointException("Cannot create CSV data for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        } else if (outputFormat.equalsIgnoreCase("xls")) {
            LOGGER.trace("Retrieving XLS-formatted data for metric " + metricName);
            try {
                OutputStream os = metricsRetriever.createXlsData(metricName, rrdFilename, startTime, endTime);
                InputStream is = new ByteArrayInputStream(((ByteArrayOutputStream) os).toByteArray());
                ResponseBuilder responseBuilder = Response.ok(is);
                responseBuilder.type("application/vnd.ms-excel");
                response = responseBuilder.build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create XLS data for metric " + metricName);
                throw new MetricsEndpointException("Cannot create XLS data for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        } else if (outputFormat.equalsIgnoreCase("ppt")) {
            LOGGER.trace("Retrieving PPT-formatted data for metric " + metricName);
            try {
                OutputStream os = metricsRetriever.createPptData(metricName, rrdFilename, startTime, endTime);
                InputStream is = new ByteArrayInputStream(((ByteArrayOutputStream) os).toByteArray());
                ResponseBuilder responseBuilder = Response.ok(is);
                responseBuilder.type("application/vnd.ms-powerpoint");
                response = responseBuilder.build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create PPT data for metric " + metricName);
                throw new MetricsEndpointException("Cannot create PPT data for metric for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        } else if (outputFormat.equalsIgnoreCase("xml")) {
            LOGGER.trace("Retrieving XML-formatted data for metric " + metricName);
            try {
                String xmlData = metricsRetriever.createXmlData(metricName, rrdFilename, startTime, endTime);
                ResponseBuilder responseBuilder = Response.ok(xmlData);
                responseBuilder.type("text/xml");
                response = responseBuilder.build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create XML data for metric " + metricName);
                throw new MetricsEndpointException("Cannot create XML data for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        } else if (outputFormat.equalsIgnoreCase("json")) {
            LOGGER.trace("Retrieving JSON-formatted data for metric " + metricName);
            try {
                String jsonData = metricsRetriever.createJsonData(metricName, rrdFilename, startTime, endTime);
                ResponseBuilder responseBuilder = Response.ok(jsonData);
                responseBuilder.type("application/json");
                response = responseBuilder.build();
            } catch (IOException | MetricsGraphException e) {
                LOGGER.warn("Could not create JSON data for metric " + metricName);
                throw new MetricsEndpointException("Cannot create JSON data for specified metric.",
                        Response.Status.BAD_REQUEST);
            }
        }

        LOGGER.trace("EXITING: getMetricsData");

        return response;
    }

    /**
     * Get list of available metrics and the associated URLs to their historical data.
     *
     * @param uriInfo
     * @return JSON-formatted response where each metric has a list of URLs (and the display text
     * for them), where each URL links to a graph of the metric's data for a specific time
     * range from current time (e.g., for last 4 hours).
     */
    @GET
    @Path("/")
    @Produces({ JSON_MIME_TYPE })
    public Response getMetricsList(@Context UriInfo uriInfo) {
        Response response = null;

        List<String> metricNames = getMetricsNames();

        Map<String, Map<String, Map<String, String>>> metrics = new LinkedHashMap<String, Map<String, Map<String, String>>>();
        for (String metricName : metricNames) {
            generateMetricsUrls(metrics, metricName, uriInfo);
        }

        String jsonText = JSONValue.toJSONString(metrics);
        LOGGER.trace(jsonText);

        response = Response.ok(jsonText).build();

        return response;
    }

    /**
     * Retrieve data for the all metrics over the given time range. The URL to access this method is
     * of the form http://<host>:<port>/report.<outputFormat> The filename extension defines the
     * desired output format returned for the report's data. Currently supported formats are xls and
     * ppt.
     * <p>
     * The XLS-formatted report will be one spreadsheet (workbook) with a worksheet per metric. The
     * PPT-formatted report will be one PowerPoint slide deck with a slide per metric. Each slide
     * will contain the metric's PNG graph.
     * <p>
     * If a summary interval is requested, the XSL report will instead contain a single table, with
     * the summarized values for each interval and metric. Cannot be used with PPT format.
     * <p>
     * Note that the time range can be specified as either a start and end date (in RFC3339 format,
     * i.e., YYYY-MM-DD'T'hh:mm:ssZ), or as an offset in seconds from the current time. These 2 time
     * range mechanisms cannot be combined, e.g., you cannot specify an end date and an offset to be
     * applied from that end date.
     * <p>
     * By default, the metric's name will be used for the y-axis label, and the metric name and time
     * range will be used for the graph's title for the report in PPT format.
     *
     * @param startDate       Specifies the start of the time range of the search on the metric's data (RFC-3339
     *                        - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Cannot be used with dateOffset
     *                        parameter.
     * @param endDate         Specifies the end of the time range of the search on the metric's data (RFC-3339 -
     *                        Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Cannot be used with dateOffset
     *                        parameter.
     * @param dateOffset      Specifies an offset, backwards from the current time, to search on the modified
     *                        time field for entries. Defined in seconds. Cannot be used with startDate or
     *                        endDate parameters.
     * @param summaryInterval One of {@link ddf.metrics.reporting.internal.rrd4j.RrdMetricsRetriever.SUMMARY_INTERVALS}
     * @param uriInfo
     * @return Response containing the report as a stream in either XLS or PPT format
     * @throws MetricsEndpointException
     */
    @GET
    @Path("/report.{outputFormat}")
    public Response getMetricsReport(@PathParam("outputFormat") String outputFormat,
            @QueryParam("startDate") String startDate, @QueryParam("endDate") String endDate,
            @QueryParam("dateOffset") String dateOffset, @QueryParam("summaryInterval") String summaryInterval,
            @Context UriInfo uriInfo) throws MetricsEndpointException {
        LOGGER.debug("ENTERING: getMetricsReport  -  outputFormat = " + outputFormat);
        LOGGER.debug("request url: " + uriInfo.getRequestUri());
        LOGGER.debug("startDate = " + startDate + ",     endDate = " + endDate);
        LOGGER.debug("dateOffset = " + dateOffset);

        Response response = null;

        // Client must specify *either* startDate and/or endDate *OR* dateOffset
        if (!StringUtils.isBlank(dateOffset)
                && (!StringUtils.isBlank(startDate) || !StringUtils.isBlank(endDate))) {
            throw new MetricsEndpointException(
                    "Cannot specify dateOffset and startDate or endDate, must specify either dateOffset only or startDate and/or endDate",
                    Response.Status.BAD_REQUEST);
        }

        long endTime;
        if (!StringUtils.isBlank(endDate)) {
            endTime = parseDate(endDate);
            LOGGER.debug("Parsed endTime = " + endTime);
        } else {
            // Default end time for metrics graphing to now (in seconds)
            Calendar now = getCalendar();
            endTime = now.getTimeInMillis() / MILLISECONDS_PER_SECOND;
            LOGGER.debug("Defaulted endTime to " + endTime);

            // Set endDate to new calculated endTime (so that endDate is displayed properly
            // in graph's title)
            endDate = dateFormatter.format(now.getTime());
        }

        long startTime;
        if (!StringUtils.isBlank(startDate)) {
            startTime = parseDate(startDate);
            LOGGER.debug("Parsed startTime = " + startTime);
        } else if (!StringUtils.isBlank(dateOffset)) {
            startTime = endTime - Long.parseLong(dateOffset);
            LOGGER.debug("Offset-computed startTime = " + startTime);

            // Set startDate to new calculated startTime (so that startDate is displayed properly
            // in graph's title)
            Calendar cal = getCalendar();
            cal.setTimeInMillis(startTime * MILLISECONDS_PER_SECOND);
            startDate = dateFormatter.format(cal.getTime());
        } else {
            // Default start time for metrics graphing to end time last 24 hours (in seconds)
            startTime = endTime - ONE_DAY_IN_SECONDS;
            LOGGER.debug("Defaulted startTime to " + startTime);

            // Set startDate to new calculated startTime (so that startDate is displayed properly
            // in graph's title)
            Calendar cal = getCalendar();
            cal.setTimeInMillis(startTime * MILLISECONDS_PER_SECOND);
            startDate = dateFormatter.format(cal.getTime());
        }

        LOGGER.debug("startDate = " + startDate + ",   endDate = " + endDate);

        List<String> metricNames = getMetricsNames();

        // Generated name for metrics file (<DDF Sitename>_<Startdate>_<EndDate>.outputFormat)
        String dispositionString = "attachment; filename=" + SystemInfo.getSiteName() + "_"
                + startDate.substring(0, 10) + "_" + endDate.substring(0, 10) + "." + outputFormat;

        try {
            if (outputFormat.equalsIgnoreCase("xls")) {
                OutputStream os = metricsRetriever.createXlsReport(metricNames, metricsDir, startTime, endTime,
                        summaryInterval);
                InputStream is = new ByteArrayInputStream(((ByteArrayOutputStream) os).toByteArray());
                ResponseBuilder responseBuilder = Response.ok(is);
                responseBuilder.type("application/vnd.ms-excel");
                responseBuilder.header("Content-Disposition", dispositionString);
                response = responseBuilder.build();
            } else if (outputFormat.equalsIgnoreCase("ppt")) {
                if (StringUtils.isNotEmpty(summaryInterval)) {
                    throw new MetricsEndpointException("Summary interval not allowed for ppt format",
                            Response.Status.BAD_REQUEST);
                }
                OutputStream os = metricsRetriever.createPptReport(metricNames, metricsDir, startTime, endTime);
                InputStream is = new ByteArrayInputStream(((ByteArrayOutputStream) os).toByteArray());
                ResponseBuilder responseBuilder = Response.ok(is);
                responseBuilder.type("application/vnd.ms-powerpoint");
                responseBuilder.header("Content-Disposition", dispositionString);
                response = responseBuilder.build();
            }
        } catch (IOException | MetricsGraphException e) {
            LOGGER.warn("Could not create " + outputFormat + " report", e);
            throw new MetricsEndpointException("Could not create report in specified output format.",
                    Response.Status.BAD_REQUEST);
        }

        LOGGER.debug("EXITING: getMetricsReport");

        return response;
    }

    private Calendar getCalendar() {
        return Calendar.getInstance(TimeZone.getTimeZone("UTC"));
    }

    /**
     * Parse date in ISO8601 format into seconds since Unix epoch.
     *
     * @param date
     * @return
     */
    protected long parseDate(String date) {
        DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTimeNoMillis();
        Date formattedDate = dateFormatter.parseDateTime(date).toDate();

        return formattedDate.getTime() / 1000;
    }

    /**
     * Generates the URLs for each time range, e.g., 15m, 1h, etc. for the specified metric.
     * <p>
     * The metric's URL info will be put in the {@code metrics} Maps passed in to this method. The
     * structure of these nested Maps are:
     * {@code Map1<String1, Map2<String2, Map3<String3,String4>>>} (Numbers added to end of Map and
     * String types so that each position could be referred to) where:
     * <ul>
     * <li>String1 = metric name, e.g., "catalogQueries"</li>
     * <li>Map2 = mapping of time range to its list of hyperlinks</li>
     * <li>String2 = time range, e.g., "15m"</li>
     * <li>Map3 = hyperlink for each format type (e.g., PNG) for each time range for each metric</li>
     * <li>String3 = display text for hyperlink, e.g., PNG</li>
     * <li>String4 = hyperlink for metric data in specific format, e.g.,
     * http://host:port/services/internal/metrics/catalogQueries.png?dateOffset=900</li>
     * </ul>
     *
     * @param metrics     nested Maps that will be populated with the metric's URL info
     * @param metricsName name of the metric to generate URLs for
     * @param uriInfo     used to extract the base URL and append the metric's path to it
     */
    protected void generateMetricsUrls(Map<String, Map<String, Map<String, String>>> metrics, String metricsName,
            UriInfo uriInfo) {
        // Generate text and hyperlink for single metric for 15 minute, 1 hour, 4 hours,
        // 12 hours, 1 day, 3 days, 1 week, 1 month, and 1 year
        Calendar cal = getCalendar();
        long endTime = cal.getTimeInMillis() / 1000;
        LOGGER.trace("Defaulted endTime to " + endTime);

        String[] supportedFormats = new String[] { "png", "csv", "xls" };

        // key=time range
        // value=list of hyperlinks (and their display text) for each supported format
        // Example:
        // key="15m"
        // value=[("PNG", "http://host:port/.../catalogQueries.png?dateOffset=900),("CSV", ...)]
        SortedMap<String, Map<String, String>> metricTimeRangeLinks = new TreeMap<String, Map<String, String>>(
                new MetricsTimeRangeComparator());
        Iterator timeRangesIter = TIME_RANGES.entrySet().iterator();
        while (timeRangesIter.hasNext()) {
            Map.Entry entry = (Map.Entry) timeRangesIter.next();
            String timeRange = (String) entry.getKey();
            long timeRangeInSeconds = (Long) entry.getValue();
            Map<String, String> metricsUrls = new LinkedHashMap<String, String>();
            for (String format : supportedFormats) {
                // CXF bug: getAbsolutePath() caches the path that was used the very first time
                // to access this REST service. For example, if client used localhost:8181/... to
                // access this service and then later a different client uses the IP address,
                // 172.18.14.169:8181/... to access this service, getAbsolutePath() will still
                // return localhost:8181 (which will not work for someone connecting remotely to
                // this service since they are probably not running DDF too).

                /*
                 * START: CXF bug with getAbsolutePath()
                 * 
                 * String uriAbsolutePath = uriInfo.getAbsolutePath().toASCIIString(); UriBuilder
                 * uriBuilder = UriBuilder.fromPath(uriAbsolutePath); uriBuilder.path(metricsName +
                 * "." + format); URI uri = uriBuilder.build(); String baseMetricsUrl =
                 * uri.toASCIIString(); String metricsUrl = baseMetricsUrl + DATE_OFFSET_QUERY +
                 * timeRangeInSeconds;
                 * 
                 * END: CXF bug
                 */

                String metricsUrl = SystemBaseUrl.getRootContext() + METRICS_SERVICE_BASE_URL + "/" + metricsName
                        + "." + format + DATE_OFFSET_QUERY + timeRangeInSeconds;

                // key=format
                // value=url for format with specified time range in seconds
                // Example:
                // "PNG", "http://host:port/.../catalogQueries.png?dateOffset=900
                metricsUrls.put(format.toUpperCase(), metricsUrl);
            }
            metricTimeRangeLinks.put(timeRange, metricsUrls);
        }

        metrics.put(metricsName, metricTimeRangeLinks);
    }

    /**
     * Returns the list of all of the RRD files in the metrics directory.
     *
     * @return
     */
    private String[] getRrdFiles() {
        FilenameFilter rrdFilter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(RRD_FILE_EXTENSION);
            }
        };

        File dir = new File(metricsDir);
        String[] rrdFiles = dir.list(rrdFilter);

        return rrdFiles;
    }

    /**
     * Returns a list of all of the metrics' names based on the list of RRD files found in the
     * metrics directory.
     *
     * @return
     */
    private List<String> getMetricsNames() {
        String[] rrdFiles = getRrdFiles();
        List<String> metricNames = new ArrayList<String>();
        if (rrdFiles != null) {
            for (String rrdFile : rrdFiles) {
                String metricsName = FilenameUtils.getFullPath(rrdFile) + FilenameUtils.getBaseName(rrdFile);
                metricNames.add(metricsName);
            }
        }

        Collections.sort(metricNames);

        LOGGER.trace("Returning " + metricNames.size() + " metrics");

        return metricNames;
    }

    void setMetricsDir(String metricsDir) {
        this.metricsDir = metricsDir;
    }

    void setMetricsRetriever(MetricsRetriever metricsRetriever) {
        this.metricsRetriever = metricsRetriever;
    }

    public void setMetricsMaxThreshold(double metricsMaxThreshold) {
        LOGGER.info("Creating new RrdMetricsRetriever with metricsMaxThreshold = " + metricsMaxThreshold);
        this.metricsMaxThreshold = metricsMaxThreshold;
        metricsRetriever = new RrdMetricsRetriever(metricsMaxThreshold);
    }

    /**
     * Comparator used to sort metric time ranges by chronological order rather than the default
     * lexigraphical order.
     */
    static class MetricsTimeRangeComparator implements Comparator, Serializable {
        private static final long serialVersionUID = 1L;

        public int compare(Object o1, Object o2) {
            String timeRange1 = (String) o1;
            String timeRange2 = (String) o2;

            Long dateOffset1 = TIME_RANGES.get(timeRange1);
            Long dateOffset2 = TIME_RANGES.get(timeRange2);

            return dateOffset1.compareTo(dateOffset2);
        }
    }
}