org.eclipse.smarthome.core.audio.internal.AudioServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.core.audio.internal.AudioServlet.java

Source

/**
 * Copyright (c) 2014-2017 by the respective copyright holders.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.eclipse.smarthome.core.audio.internal;

import java.io.IOException;
import java.io.InputStream;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.audio.AudioException;
import org.eclipse.smarthome.core.audio.AudioFormat;
import org.eclipse.smarthome.core.audio.AudioHTTPServer;
import org.eclipse.smarthome.core.audio.AudioStream;
import org.eclipse.smarthome.core.audio.FixedLengthAudioStream;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A servlet that serves audio streams via HTTP.
 *
 * @author Kai Kreuzer - Initial contribution and API
 *
 */
public class AudioServlet extends HttpServlet implements AudioHTTPServer {

    private static final long serialVersionUID = -3364664035854567854L;

    private static final String SERVLET_NAME = "/audio";

    private final Logger logger = LoggerFactory.getLogger(AudioServlet.class);

    private Map<String, AudioStream> oneTimeStreams = new ConcurrentHashMap<>();
    private Map<String, FixedLengthAudioStream> multiTimeStreams = new ConcurrentHashMap<>();
    private Map<String, Long> streamTimeouts = new ConcurrentHashMap<>();

    protected HttpService httpService;

    protected void setHttpService(HttpService httpService) {
        this.httpService = httpService;

        try {
            logger.debug("Starting up the audio servlet at " + SERVLET_NAME);
            Hashtable<String, String> props = new Hashtable<String, String>();
            httpService.registerServlet(SERVLET_NAME, this, props, createHttpContext());
        } catch (NamespaceException e) {
            logger.error("Error during servlet startup", e);
        } catch (ServletException e) {
            logger.error("Error during servlet startup", e);
        }
    }

    protected void unsetHttpService(HttpService httpService) {
        httpService.unregister(SERVLET_NAME);
        this.httpService = null;
    }

    /**
     * Creates an {@link HttpContext}.
     *
     * @return an {@link HttpContext} that grants anonymous access
     */
    protected HttpContext createHttpContext() {
        // TODO: Once we have a role-based permission system in place, we need to make sure that we create an
        // HttpContext here, which allows accessing the servlet without any authentication.
        HttpContext httpContext = httpService.createDefaultHttpContext();
        return httpContext;
    }

    private InputStream prepareInputStream(final String streamId, final HttpServletResponse resp)
            throws AudioException {
        final AudioStream stream;
        final boolean multiAccess;
        if (oneTimeStreams.containsKey(streamId)) {
            stream = oneTimeStreams.remove(streamId);
            multiAccess = false;
        } else if (multiTimeStreams.containsKey(streamId)) {
            stream = multiTimeStreams.get(streamId);
            multiAccess = true;
        } else {
            return null;
        }

        logger.debug("Stream to serve is {}", streamId);

        // try to set the content-type, if possible
        final String mimeType;
        if (stream.getFormat().getCodec() == AudioFormat.CODEC_MP3) {
            mimeType = "audio/mpeg";
        } else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_WAVE) {
            mimeType = "audio/wav";
        } else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_OGG) {
            mimeType = "audio/ogg";
        } else {
            mimeType = null;
        }
        if (mimeType != null) {
            resp.setContentType(mimeType);
        }

        // try to set the content-length, if possible
        if (stream instanceof FixedLengthAudioStream) {
            final Long size = ((FixedLengthAudioStream) stream).length();
            if (size != null) {
                resp.setContentLength(size.intValue());
            }
        }

        if (multiAccess) {
            // we need to care about concurrent access and have a separate stream for each thread
            return ((FixedLengthAudioStream) stream).getClonedStream();
        } else {
            return stream;
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        removeTimedOutStreams();

        final String streamId = StringUtils
                .substringBefore(StringUtils.substringAfterLast(req.getRequestURI(), "/"), ".");

        try (final InputStream stream = prepareInputStream(streamId, resp)) {
            if (stream == null) {
                logger.debug("Received request for invalid stream id at {}", req.getRequestURI());
                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            } else {
                IOUtils.copy(stream, resp.getOutputStream());
                resp.flushBuffer();
            }
        } catch (final AudioException ex) {
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private synchronized void removeTimedOutStreams() {
        for (String streamId : multiTimeStreams.keySet()) {
            if (streamTimeouts.get(streamId) < System.nanoTime()) {
                // the stream has expired, we need to remove it!
                FixedLengthAudioStream stream = multiTimeStreams.remove(streamId);
                streamTimeouts.remove(streamId);
                IOUtils.closeQuietly(stream);
                stream = null;
                logger.debug("Removed timed out stream {}", streamId);
            }
        }
    }

    @Override
    public String serve(AudioStream stream) {
        String streamId = UUID.randomUUID().toString();
        oneTimeStreams.put(streamId, stream);
        return getRelativeURL(streamId);
    }

    @Override
    public String serve(FixedLengthAudioStream stream, int seconds) {
        String streamId = UUID.randomUUID().toString();
        multiTimeStreams.put(streamId, stream);
        streamTimeouts.put(streamId, System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds));
        return getRelativeURL(streamId);
    }

    private String getRelativeURL(String streamId) {
        return SERVLET_NAME + "/" + streamId;
    }

}