org.eclipse.smarthome.ui.internal.chart.ChartServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.ui.internal.chart.ChartServlet.java

Source

/**
 * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.smarthome.ui.internal.chart;

import java.awt.image.BufferedImage;
import java.io.EOFException;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.BooleanUtils;
import org.eclipse.smarthome.core.items.ItemNotFoundException;
import org.eclipse.smarthome.io.http.servlet.SmartHomeServlet;
import org.eclipse.smarthome.ui.chart.ChartProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;

/**
 * This servlet generates time-series charts for a given set of items. It
 * accepts the following HTTP parameters:
 * <ul>
 * <li>w: width in pixels of image to generate</li>
 * <li>h: height in pixels of image to generate</li>
 * <li>period: the time span for the x-axis. Value can be h,4h,8h,12h,D,3D,W,2W,M,2M,4M,Y</li>
 * <li>items: A comma separated list of item names to display</li>
 * <li>groups: A comma separated list of group names, whose members should be displayed</li>
 * <li>service: The persistence service name. If not supplied the first service found will be used.</li>
 * <li>theme: The chart theme to use. If not supplied the chart provider uses a default theme.</li>
 * <li>dpi: The DPI (dots per inch) value. If not supplied, a default is used.</code></li>
 * <li>legend: Show the legend? If not supplied, the ChartProvider should make his own decision.</li>
 * </ul>
 *
 * @author Chris Jackson
 * @author Holger Reichert - Support for themes, DPI, legend hiding
 *
 */
@Component(immediate = true, service = ChartServlet.class, configurationPid = "org.eclipse.smarthome.chart", property = {
        "service.pid=org.eclipse.smarthome.chart", "service.config.description.uri=system:chart",
        "service.config.label=Charts", "service.config.category=system" })
public class ChartServlet extends SmartHomeServlet {

    private static final long serialVersionUID = 7700873790924746422L;
    private static final int CHART_HEIGHT = 240;
    private static final int CHART_WIDTH = 480;
    private static final String DATE_FORMAT = "yyyyMMddHHmm";

    private String providerName = "default";
    private int defaultHeight = CHART_HEIGHT;
    private int defaultWidth = CHART_WIDTH;
    private double scale = 1.0;
    private int maxWidth = -1;

    // The URI of this servlet
    public static final String SERVLET_NAME = "/chart";

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

    static {
        PERIODS.put("h", 3600000L);
        PERIODS.put("4h", 14400000L);
        PERIODS.put("8h", 28800000L);
        PERIODS.put("12h", 43200000L);
        PERIODS.put("D", 86400000L);
        PERIODS.put("2D", 172800000L);
        PERIODS.put("3D", 259200000L);
        PERIODS.put("W", 604800000L);
        PERIODS.put("2W", 1209600000L);
        PERIODS.put("M", 2592000000L);
        PERIODS.put("2M", 5184000000L);
        PERIODS.put("4M", 10368000000L);
        PERIODS.put("Y", 31536000000L);
    }

    protected static Map<String, ChartProvider> chartProviders = new ConcurrentHashMap<String, ChartProvider>();

    @Override
    @Reference
    public void setHttpService(HttpService httpService) {
        super.setHttpService(httpService);
    }

    @Override
    public void unsetHttpService(HttpService httpService) {
        super.unsetHttpService(httpService);
    }

    @Override
    @Reference
    public void setHttpContext(HttpContext httpContext) {
        super.setHttpContext(httpContext);
    }

    @Override
    public void unsetHttpContext(HttpContext httpContext) {
        super.unsetHttpContext(httpContext);
    }

    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
    public void addChartProvider(ChartProvider provider) {
        chartProviders.put(provider.getName(), provider);
    }

    public void removeChartProvider(ChartProvider provider) {
        chartProviders.remove(provider.getName());
    }

    public static Map<String, ChartProvider> getChartProviders() {
        return chartProviders;
    }

    @Activate
    protected void activate(Map<String, Object> config) {
        super.activate(SERVLET_NAME);
        applyConfig(config);
    }

    @Deactivate
    protected void deactivate() {
        super.deactivate(SERVLET_NAME);
    }

    @Modified
    protected void modified(Map<String, Object> config) {
        applyConfig(config);
    }

    /**
     * Handle the initial or a changed configuration.
     *
     * @param config the configuration
     */
    private void applyConfig(Map<String, Object> config) {
        if (config == null) {
            return;
        }

        final String providerNameString = Objects.toString(config.get("provider"), null);
        if (providerNameString != null) {
            providerName = providerNameString;
        }

        final String defaultHeightString = Objects.toString(config.get("defaultHeight"), null);
        if (defaultHeightString != null) {
            try {
                defaultHeight = Integer.parseInt(defaultHeightString);
            } catch (NumberFormatException e) {
                logger.warn("'{}' is not a valid integer value for the defaultHeight parameter.",
                        defaultHeightString);
            }
        }

        final String defaultWidthString = Objects.toString(config.get("defaultWidth"), null);
        if (defaultWidthString != null) {
            try {
                defaultWidth = Integer.parseInt(defaultWidthString);
            } catch (NumberFormatException e) {
                logger.warn("'{}' is not a valid integer value for the defaultWidth parameter.",
                        defaultWidthString);
            }
        }

        final String scaleString = Objects.toString(config.get("scale"), null);
        if (scaleString != null) {
            try {
                scale = Double.parseDouble(scaleString);
                // Set scale to normal if the custom value is unrealistically low
                if (scale < 0.1) {
                    scale = 1.0;
                }
            } catch (NumberFormatException e) {
                logger.warn("'{}' is not a valid number value for the scale parameter.", scaleString);
            }
        }

        final String maxWidthString = Objects.toString(config.get("maxWidth"), null);
        if (maxWidthString != null) {
            try {
                maxWidth = Integer.parseInt(maxWidthString);
            } catch (NumberFormatException e) {
                logger.warn("'{}' is not a valid integer value for the maxWidth parameter.", maxWidthString);
            }
        }
    }

    @SuppressWarnings({ "null" })
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        logger.debug("Received incoming chart request: {}", req);

        int width = defaultWidth;
        String w = req.getParameter("w");
        if (w != null) {
            try {
                width = Integer.parseInt(w);
            } catch (NumberFormatException e) {
                logger.debug("Ignoring invalid value '{}' for HTTP request parameter 'w'", w);
            }
        }
        int height = defaultHeight;
        String h = req.getParameter("h");
        if (h != null) {
            try {
                Double d = Double.parseDouble(h) * scale;
                height = d.intValue();
            } catch (NumberFormatException e) {
                logger.debug("Ignoring invalid value '{}' for HTTP request parameter 'h'", h);
            }
        }

        // To avoid ambiguity you are not allowed to specify period, begin and end time at the same time.
        if (req.getParameter("period") != null && req.getParameter("begin") != null
                && req.getParameter("end") != null) {
            res.sendError(HttpServletResponse.SC_BAD_REQUEST,
                    "Do not specify the three parameters period, begin and end at the same time.");
            return;
        }

        // Read out the parameter period, begin and end and save them.
        Date timeBegin = null;
        Date timeEnd = null;

        Long period = PERIODS.get(req.getParameter("period"));
        if (period == null) {
            // use a day as the default period
            period = PERIODS.get("D");
        }

        if (req.getParameter("begin") != null) {
            try {
                timeBegin = new SimpleDateFormat(DATE_FORMAT).parse(req.getParameter("begin"));
            } catch (ParseException e) {
                res.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "Begin and end must have this format: " + DATE_FORMAT + ".");
                return;
            }
        }

        if (req.getParameter("end") != null) {
            try {
                timeEnd = new SimpleDateFormat(DATE_FORMAT).parse(req.getParameter("end"));
            } catch (ParseException e) {
                res.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "Begin and end must have this format: " + DATE_FORMAT + ".");
                return;
            }
        }

        // Set begin and end time and check legality.
        if (timeBegin == null && timeEnd == null) {
            timeEnd = new Date();
            timeBegin = new Date(timeEnd.getTime() - period);
            logger.debug("No begin or end is specified, use now as end and now-period as begin.");
        } else if (timeEnd == null) {
            timeEnd = new Date(timeBegin.getTime() + period);
            logger.debug("No end is specified, use begin + period as end.");
        } else if (timeBegin == null) {
            timeBegin = new Date(timeEnd.getTime() - period);
            logger.debug("No begin is specified, use end-period as begin");
        } else if (timeEnd.before(timeBegin)) {
            throw new ServletException("The end is before the begin.");
        }

        // If a persistence service is specified, find the provider
        String serviceName = req.getParameter("service");

        ChartProvider provider = getChartProviders().get(providerName);
        if (provider == null) {
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not get chart provider.");
            return;
        }

        // Read out the parameter 'dpi'
        Integer dpi = null;
        if (req.getParameter("dpi") != null) {
            try {
                dpi = Integer.valueOf(req.getParameter("dpi"));
            } catch (NumberFormatException e) {
                res.sendError(HttpServletResponse.SC_BAD_REQUEST, "dpi parameter is invalid");
                return;
            }
            if (dpi <= 0) {
                res.sendError(HttpServletResponse.SC_BAD_REQUEST, "dpi parameter is <= 0");
                return;
            }
        }

        // Read out parameter 'legend'
        Boolean legend = null;
        if (req.getParameter("legend") != null) {
            legend = BooleanUtils.toBoolean(req.getParameter("legend"));
        }

        if (maxWidth > 0 && width > maxWidth) {
            height = Math.round((float) height / (float) width * maxWidth);
            if (dpi != null) {
                dpi = Math.round((float) dpi / (float) width * maxWidth);
            }
            width = maxWidth;
        }

        // Set the content type to that provided by the chart provider
        res.setContentType("image/" + provider.getChartType());
        logger.debug("chart building with width {} height {} dpi {}", width, height, dpi);
        try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(res.getOutputStream())) {
            BufferedImage chart = provider.createChart(serviceName, req.getParameter("theme"), timeBegin, timeEnd,
                    height, width, req.getParameter("items"), req.getParameter("groups"), dpi, legend);
            ImageIO.write(chart, provider.getChartType().toString(), imageOutputStream);
            logger.debug("Chart successfully generated and written to the response.");
        } catch (ItemNotFoundException e) {
            logger.debug("{}", e.getMessage());
            res.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.warn("Illegal argument in chart: {}", e.getMessage());
            res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Illegal argument in chart: " + e.getMessage());
        } catch (IIOException | EOFException e) {
            // this can happen if the request is terminated while the image is streamed, see
            // https://github.com/openhab/openhab-distro/issues/684
            logger.debug("Failed writing image to response stream", e);
        } catch (RuntimeException e) {
            if (logger.isDebugEnabled()) {
                // we also attach the stack trace
                logger.warn("Chart generation failed: {}", e.getMessage(), e);
            } else {
                logger.warn("Chart generation failed: {}", e.getMessage());
            }
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
    }

}