All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.tidalwave.bluemarine2.upnp.mediaserver.impl.resourceserver.DefaultResourceServer Maven / Gradle / Ivy

/*
 * #%L
 * *********************************************************************************************************************
 *
 * blueMarine2 - Semantic Media Center
 * http://bluemarine2.tidalwave.it - git clone https://bitbucket.org/tidalwave/bluemarine2-src.git
 * %%
 * Copyright (C) 2015 - 2017 Tidalwave s.a.s. (http://tidalwave.it)
 * %%
 *
 * *********************************************************************************************************************
 *
 * 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
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * *********************************************************************************************************************
 *
 * $Id$
 *
 * *********************************************************************************************************************
 * #L%
 */
package it.tidalwave.bluemarine2.upnp.mediaserver.impl.resourceserver;

import javax.annotation.Nonnull;
import javax.annotation.PreDestroy;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import it.tidalwave.messagebus.annotation.ListensTo;
import it.tidalwave.messagebus.annotation.SimpleMessageSubscriber;
import it.tidalwave.bluemarine2.util.PowerOnNotification;
import it.tidalwave.bluemarine2.model.AudioFile;
import it.tidalwave.bluemarine2.model.ModelPropertyNames;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static javax.servlet.http.HttpServletResponse.*;
import static it.tidalwave.bluemarine2.util.Miscellaneous.normalizedPath;

/***********************************************************************************************************************
 *
 * @author  Fabrizio Giudici
 * @version $Id$
 *
 **********************************************************************************************************************/
@SimpleMessageSubscriber @Slf4j
public class DefaultResourceServer implements ResourceServer
  {
    @Getter
    private String ipAddress = "";

    @Getter
    private int port;

    private Server server;

    private Path rootPath;

    protected static class Range
      {
        private final long start;
        private final long end;
        private final long length;
        private final long total;

        /**
         * Construct a byte range.
         * @param start Start of the byte range.
         * @param end End of the byte range.
         * @param total Total length of the byte source.
         */
        public Range (final long start, final long end, final long total)
          {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
          }
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    private final ServletAdapter servlet = new ServletAdapter()
      {
        private static final long serialVersionUID = -387471254552805904L;

        private static final int DEFAULT_BUFFER_SIZE = 64 * 1024;

        @Override
        protected void doGet (final @Nonnull HttpServletRequest request, final @Nonnull HttpServletResponse response)
          throws ServletException, IOException
          {
            final Path resourcePath =
                    normalizedPath(rootPath.resolve(urlDecoded(request.getRequestURI().replaceAll("^/", ""))));
            log.debug(">>>> resource path: {}", resourcePath);

//            if (isTroubled(resourcePath))
//              {
//                log.error(">>>> path affected by BMT-46: {}", resourcePath);
//                response.setStatus(SC_INTERNAL_SERVER_ERROR);
//                return;
//              }

            if (!Files.exists(resourcePath))
              {
                log.error(">>>> resource not found: {}", resourcePath);
                response.setStatus(SC_NOT_FOUND);
                return;
              }

            if (Files.isDirectory(resourcePath))
              {
                log.error(">>>> cannot serve directories: {}", resourcePath);
                response.setStatus(SC_UNAUTHORIZED);
                return;
              }

            final long length = (int)Files.size(resourcePath);
            final Range fullRange = new Range(0, length - 1, length);
            final List ranges = new ArrayList<>();

            final String range = request.getHeader("Range");

            if (range != null)
              {
                // Range header should match format "bytes=n-n,n-n,n-n...".
                if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$"))
                  {
                    log.error("Invalid range: {}", range);
                    response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                    response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                  }

                // If any valid If-Range header, then process each part of byte range.
                for (final String part : range.substring(6).split(","))
                  {
                    // Assuming a file with length of 100, the following examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = subStringOrMinusOne(part, 0, part.indexOf("-"));
                    long end = subStringOrMinusOne(part, part.indexOf("-") + 1, part.length());

                    if (start == -1)
                      {
                        start = length - end;
                        end = length - 1;
                      }
                    else if ((end == -1) || (end > length - 1))
                      {
                        end = length - 1;
                      }

                    if (start > end)
                      {
                        log.error("Invalid range: {}", range);
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                      }

                    ranges.add(new Range(start, end, length));
                  }
              }

            if (ranges.size() > 1)
              {
                log.error("Can't support multi-range: {}", range);
                response.setStatus(SC_INTERNAL_SERVER_ERROR);
                return;
              }

            response.setContentType("audio/mpeg");
    //                response.setContentType(Files.probeContentType(resourcePath)); FIXME
            final Range r = ranges.stream().findFirst().orElse(fullRange);
            response.setBufferSize(0);
            response.setContentLength((int)r.length);

            if (!fullRange.equals(r))
              {
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setStatus(SC_PARTIAL_CONTENT);
              }

            try (final RandomAccessFile input = new RandomAccessFile(resourcePath.toFile(), "r");
                 final OutputStream output = response.getOutputStream())
              {
                // FIXME: use memory mapped i/o
                copy(input, output, r.start, r.length);
                output.flush(); // force EOF in case
              }
            catch (EOFException e)
              {
                log.debug("EOF - probably client closed connection");
              }
          }

        /**
         * Returns a substring of the given string value from the given begin index to the given end
         * index as a long. If the substring is empty, then -1 will be returned
         * @param value The string value to return a substring as long for.
         * @param beginIndex The begin index of the substring to be returned as long.
         * @param endIndex The end index of the substring to be returned as long.
         * @return A substring of the given string value as long or -1 if substring is empty.
         */
        private long subStringOrMinusOne (String value, int beginIndex, int endIndex)
          {
            String substring = value.substring(beginIndex, endIndex);
            return (substring.length() > 0) ? Long.parseLong(substring) : -1;
          }

        /**
         * Copy the given byte range of the given input to the given output.
         * @param input The input to copy the given range to the given output for.
         * @param output The output to copy the given range from the given input for.
         * @param start Start of the byte range.
         * @param length Length of the byte range.
         * @throws IOException If something fails at I/O level.
         */
        private void copy (RandomAccessFile input, OutputStream output, long start, long length)
          throws IOException
          {
            final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int read;

            if (input.length() == length)
              {
                // Write full range.
                while ((read = input.read(buffer)) > 0)
                  {
                    output.write(buffer, 0, read);
                  }
              }
            else
              {
                // Write partial range.
                input.seek(start);
                long toRead = length;

                while ((read = input.read(buffer)) > 0)
                  {
                    if ((toRead -= read) > 0)
                      {
                        output.write(buffer, 0, read);
                      }
                    else
                      {
                        output.write(buffer, 0, (int) toRead + read);
                        break;
                      }
                  }
              }
          }
      };

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    /* VisibleForTesting */ void onPowerOnNotification (final @ListensTo @Nonnull PowerOnNotification notification)
      throws Exception
      {
        log.info("onPowerOnNotification({})", notification);
        rootPath = notification.getProperties().get(ModelPropertyNames.ROOT_PATH);
        ipAddress = getNonLoopbackIPv4Address().getHostAddress();
        server = new Server(InetSocketAddress.createUnresolved(ipAddress, Integer.getInteger("port", 0)));
        server.setHandler(servlet.asHandler());
        server.start();
        port = server.getConnectors()[0].getLocalPort(); // jetty 8
//        port = ((ServerConnector)server.getConnectors()[0]).getLocalPort(); // jetty 9
        log.info(">>>> resource server jetty started at {}:{} serving resources at {}", ipAddress, port, rootPath);
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private InetAddress getNonLoopbackIPv4Address()
      throws SocketException
      {
        for (final Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements() ; )
          {
            final NetworkInterface itf =  en.nextElement();

            if (!itf.getName().startsWith("docker"))
              {
                for (final Enumeration ee = itf.getInetAddresses(); ee.hasMoreElements() ;)
                  {
                    final InetAddress address = ee.nextElement();

                    if (!address.isLoopbackAddress() && (address instanceof Inet4Address))
                      {
                        return address;
                      }
                  }
              }
          }

        log.warn("Returning loopback address!");
        return InetAddress.getLoopbackAddress();
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @PreDestroy // FIXME: user PowerOffNotification
    private void shutDown()
      throws Exception
      {
        server.stop();
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @Override
    public String urlForResource (final @Nonnull AudioFile resource)
      {
        final Path path = rootPath.relativize(resource.getPath());
        final String s = StreamSupport.stream(path.spliterator(), false)
                                      .map(p -> urlEncoded(p.toString()))
                                      .collect(Collectors.joining("/"));

        return "http://" + ipAddress + ":" + port + "/" + s;
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private static String urlDecoded (final @Nonnull String string)
      {
        try
          {
            return URLDecoder.decode(string, "UTF-8");
          }
        catch (UnsupportedEncodingException e)
          {
            throw new RuntimeException(e);
          }
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private static String urlEncoded (final @Nonnull String string)
      {
        try
          {
            return URLEncoder.encode(string, "UTF-8");
          }
        catch (UnsupportedEncodingException e)
          {
            throw new RuntimeException(e);
          }
      }
  }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy