Content of file MusicResourcesController.java.html

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../../jacoco-resources/report.gif" type="image/gif"/><title>MusicResourcesController.java</title><link rel="stylesheet" href="../../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../../index.html" class="el_report">blueMarine II :: Media Server :: UPnP</a> &gt; <a href="../index.html" class="el_bundle">it-tidalwave-bluemarine2-rest</a> &gt; <a href="index.source.html" class="el_package">it.tidalwave.bluemarine2.rest.impl</a> &gt; <span class="el_source">MusicResourcesController.java</span></div><h1>MusicResourcesController.java</h1><pre class="source lang-java linenums"><span class="fc" id="L1">/*</span>
 * *********************************************************************************************************************
 *
 * blueMarine II: Semantic Media Centre
 * http://tidalwave.it/projects/bluemarine2
 *
 * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;); 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 &quot;AS IS&quot; 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.
 *
 * *********************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/bluemarine2-src
 * git clone https://github.com/tidalwave-it/bluemarine2-src
 *
 * *********************************************************************************************************************
 */
package it.tidalwave.bluemarine2.rest.impl;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import java.io.IOException;
import java.net.URLEncoder;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
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.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import it.tidalwave.util.Finder;
import it.tidalwave.util.Id;
import it.tidalwave.util.annotation.VisibleForTesting;
import it.tidalwave.messagebus.annotation.ListensTo;
import it.tidalwave.messagebus.annotation.SimpleMessageSubscriber;
import it.tidalwave.bluemarine2.model.MediaCatalog;
import it.tidalwave.bluemarine2.model.audio.AudioFile;
import it.tidalwave.bluemarine2.model.role.AudioFileSupplier;
import it.tidalwave.bluemarine2.model.spi.SourceAwareFinder;
import it.tidalwave.bluemarine2.message.PersistenceInitializedNotification;
import it.tidalwave.bluemarine2.rest.impl.resource.AudioFileResource;
import it.tidalwave.bluemarine2.rest.impl.resource.DetailedRecordResource;
import it.tidalwave.bluemarine2.rest.impl.resource.RecordResource;
import it.tidalwave.bluemarine2.rest.impl.resource.TrackResource;
import lombok.extern.slf4j.Slf4j;
import static java.util.stream.Collectors.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.http.HttpHeaders.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;
import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
import static it.tidalwave.role.ui.Displayable._Displayable_;
import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
import static it.tidalwave.bluemarine2.model.role.AudioFileSupplier._AudioFileSupplier_;

/***********************************************************************************************************************
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
<span class="fc" id="L79">@RestController @SimpleMessageSubscriber @Slf4j</span>
<span class="fc" id="L80">public class MusicResourcesController</span>
  {
    static interface Streamable&lt;ENTITY, FINDER extends SourceAwareFinder&lt;FINDER, ENTITY&gt;&gt; extends SourceAwareFinder&lt;ENTITY, FINDER&gt;
      {
        public Stream&lt;ENTITY&gt; stream();
      }

    @ResponseStatus(value = NOT_FOUND)
<span class="fc" id="L88">    static class NotFoundException extends RuntimeException</span>
      {
        private static final long serialVersionUID = 3099300911009857337L;
      }

    @ResponseStatus(value = SERVICE_UNAVAILABLE)
<span class="nc" id="L94">    static class UnavailableException extends RuntimeException</span>
      {
        private static final long serialVersionUID = 3644567083880573896L;
      }

    @Inject
    private MediaCatalog catalog;

    private volatile boolean persistenceInitialized;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @VisibleForTesting void onPersistenceInitializedNotification (@ListensTo final PersistenceInitializedNotification notification)
      throws IOException
      {
<span class="nc" id="L111">        log.info(&quot;onPersistenceInitializedNotification({})&quot;, notification);</span>
<span class="nc" id="L112">        persistenceInitialized = false;</span>
<span class="nc" id="L113">      }</span>

    /*******************************************************************************************************************
     *
     * Exports record resources.
     *
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the records
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/record&quot;, produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public List&lt;RecordResource&gt; getRecords (@RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                            @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L129">        log.info(&quot;getRecords({}, {})&quot;, source, fallback);</span>
<span class="fc" id="L130">        checkStatus();</span>
<span class="fc" id="L131">        return finalized(catalog.findRecords(), source, fallback, RecordResource::new);</span>
      }

    /*******************************************************************************************************************
     *
     * Exports a single record resource.
     *
     * @param   id          the record id
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the record
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/record/{id}&quot;, produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public DetailedRecordResource getRecord (@PathVariable final String id,
                                             @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                             @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L150">        log.info(&quot;getRecord({}, {}, {})&quot;, id, source, fallback);</span>
<span class="fc" id="L151">        checkStatus();</span>
<span class="fc" id="L152">        final List&lt;TrackResource&gt; tracks = finalized(catalog.findTracks().inRecord(Id.of(id)), source, fallback, TrackResource::new);</span>
<span class="fc" id="L153">        return single(finalized(catalog.findRecords().withId(Id.of(id)), source, fallback,</span>
<span class="fc" id="L154">                                record -&gt; new DetailedRecordResource(record, tracks)));</span>
      }

    /*******************************************************************************************************************
     *
     * Exports the cover art of a record.
     *
     * @param   id          the record id
     * @return              the cover art image
     *
     ******************************************************************************************************************/
    @RequestMapping(value = &quot;/record/{id}/coverart&quot;)
    public ResponseEntity&lt;byte[]&gt; getRecordCoverArt (@PathVariable final String id)
      {
<span class="fc" id="L168">        log.info(&quot;getRecordCoverArt({})&quot;, id);</span>
<span class="fc" id="L169">        checkStatus();</span>
<span class="fc" id="L170">        return catalog.findTracks().inRecord(Id.of(id))</span>
<span class="fc" id="L171">                                   .stream()</span>
<span class="fc" id="L172">                                   .flatMap(track -&gt; track.asMany(_AudioFileSupplier_).stream())</span>
<span class="fc" id="L173">                                   .map(AudioFileSupplier::getAudioFile)</span>
<span class="fc" id="L174">                                   .flatMap(af -&gt; af.getMetadata().getAll(ARTWORK).stream())</span>
<span class="fc" id="L175">                                   .findAny()</span>
<span class="fc" id="L176">                                   .map(bytes -&gt; bytesResponse(bytes, &quot;image&quot;, &quot;jpeg&quot;, &quot;coverart.jpg&quot;))</span>
<span class="fc" id="L177">                                   .orElseThrow(NotFoundException::new);</span>
      }

    /*******************************************************************************************************************
     *
     * Exports track resources.
     *
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the tracks
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/track&quot;, produces  = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public List&lt;TrackResource&gt; getTracks (@RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                          @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L194">        log.info(&quot;getTracks({}, {})&quot;, source, fallback);</span>
<span class="fc" id="L195">        checkStatus();</span>
<span class="fc" id="L196">        return finalized(catalog.findTracks(), source, fallback, TrackResource::new);</span>
      }

    /*******************************************************************************************************************
     *
     * Exports a single track resource.
     *
     * @param   id          the track id
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the track
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/track/{id}&quot;, produces  = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public TrackResource getTrack (@PathVariable final String id,
                                   @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                   @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L215">        log.info(&quot;getTrack({}, {}, {})&quot;, id, source, fallback);</span>
<span class="fc" id="L216">        checkStatus();</span>
<span class="fc" id="L217">        return single(finalized(catalog.findTracks().withId(Id.of(id)), source, fallback, TrackResource::new));</span>
      }

    /*******************************************************************************************************************
     *
     * Exports audio file resources.
     *
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the audio files
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/audiofile&quot;, produces  = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public List&lt;AudioFileResource&gt; getAudioFiles (@RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                                  @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L234">        log.info(&quot;getAudioFiles({}, {})&quot;, source, fallback);</span>
<span class="fc" id="L235">        checkStatus();</span>
<span class="fc" id="L236">        return finalized(catalog.findAudioFiles(), source, fallback, AudioFileResource::new);</span>
      }

    /*******************************************************************************************************************
     *
     * Exports a single audio file resource.
     *
     * @param   id          the audio file id
     * @param   source      the data source
     * @param   fallback    the fallback data source
     * @return              the JSON representation of the audio file
     *
     ******************************************************************************************************************/
    @ResponseBody
    @RequestMapping(value = &quot;/audiofile/{id}&quot;, produces  = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
    public AudioFileResource getAudioFile (@PathVariable final String id,
                                           @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String source,
                                           @RequestParam(required = false, defaultValue = &quot;embedded&quot;) final String fallback)
      {
<span class="fc" id="L255">        log.info(&quot;getAudioFile({}, {}, {})&quot;, id, source, fallback);</span>
<span class="fc" id="L256">        checkStatus();</span>
<span class="fc" id="L257">        return single(finalized(catalog.findAudioFiles().withId(Id.of(id)), source, fallback, AudioFileResource::new));</span>
      }

    /*******************************************************************************************************************
     *
     * @param   id          the audio file id
     * @param   rangeHeader the &quot;Range&quot; HTTP header
     * @return              the binary contents
     *
     ******************************************************************************************************************/
    @RequestMapping(value = &quot;/audiofile/{id}/content&quot;)
    public ResponseEntity&lt;ResourceRegion&gt; getAudioFileContent (
            @PathVariable final String id,
            @RequestHeader(name = &quot;Range&quot;, required = false) final String rangeHeader)
      {
<span class="fc" id="L272">        log.info(&quot;getAudioFileContent({})&quot;, id);</span>
<span class="fc" id="L273">        checkStatus();</span>
<span class="fc" id="L274">        return catalog.findAudioFiles().withId(Id.of(id)).optionalResult()</span>
<span class="fc" id="L275">                                                          .map(_f(af -&gt; audioFileContentResponse(af, rangeHeader)))</span>
<span class="fc" id="L276">                                                          .orElseThrow(NotFoundException::new);</span>
      }

    /*******************************************************************************************************************
     *
     * @param   id          the audio file id
     * @return              the binary contents
     *
     ******************************************************************************************************************/
    @RequestMapping(value = &quot;/audiofile/{id}/coverart&quot;)
    public ResponseEntity&lt;byte[]&gt; getAudioFileCoverArt (@PathVariable final String id)
      {
<span class="fc" id="L288">        log.info(&quot;getAudioFileCoverArt({})&quot;, id);</span>
<span class="fc" id="L289">        checkStatus();</span>
<span class="fc" id="L290">        final Optional&lt;AudioFile&gt; audioFile = catalog.findAudioFiles().withId(Id.of(id)).optionalResult();</span>
<span class="fc" id="L291">        log.debug(&quot;&gt;&gt;&gt;&gt; audioFile: {}&quot;, audioFile);</span>
<span class="fc" id="L292">        return audioFile.flatMap(file -&gt; file.getMetadata().getAll(ARTWORK).stream().findFirst())</span>
<span class="fc" id="L293">                        .map(bytes -&gt; bytesResponse(bytes, &quot;image&quot;, &quot;jpeg&quot;, &quot;coverart.jpg&quot;))</span>
<span class="fc" id="L294">                        .orElseThrow(NotFoundException::new);</span>
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    @Nonnull
    private &lt;T&gt; T single (@Nonnull final List&lt;T&gt; list)
      {
<span class="fc bfc" id="L303" title="All 2 branches covered.">        if (list.isEmpty())</span>
          {
<span class="fc" id="L305">            throw new NotFoundException();</span>
          }

<span class="fc" id="L308">        return list.get(0);</span>
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    @Nonnull
    private &lt;ENTITY, FINDER extends SourceAwareFinder&lt;ENTITY, FINDER&gt;, JSON&gt;
        List&lt;JSON&gt; finalized (@Nonnull final FINDER finder,
                              @Nonnull final String source,
                              @Nonnull final String fallback,
                              @Nonnull final Function&lt;ENTITY, JSON&gt; mapper)
      {
<span class="fc" id="L321">        final FINDER f = finder.importedFrom(Id.of(source)).withFallback(Id.of(fallback));</span>
<span class="fc" id="L322">        return ((Finder&lt;ENTITY&gt;)f) // FIXME: hacky, because SourceAwareFinder does not extends Finder</span>
hacky, because SourceAwareFinder does not extends Finder
<span class="fc" id="L323"> .stream()</span> <span class="fc" id="L324"> .map(mapper)</span> <span class="fc" id="L325"> .collect(toList());</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ @Nonnull private ResponseEntity&lt;ResourceRegion&gt; audioFileContentResponse (@Nonnull final AudioFile file, @Nullable final String rangeHeader) throws IOException { <span class="fc" id="L336"> final long length = file.getSize();</span> <span class="fc" id="L337"> final List&lt;Range&gt; ranges = Range.fromHeader(rangeHeader, length);</span> <span class="pc bpc" id="L339" title="1 of 2 branches missed."> if (ranges.size() &gt; 1)</span> { <span class="nc" id="L341"> throw new RuntimeException(&quot;Can't support multi-range&quot; + ranges); // FIXME</span> } // E.g. HTML5 &lt;audio&gt; crashes if fed with too many data. <span class="pc bpc" id="L345" title="1 of 2 branches missed."> final long maxSize = (rangeHeader != null) ? 1024*1024 : length;</span> <span class="fc" id="L346"> final Range fullRange = Range.full(length);</span> <span class="fc" id="L347"> final Range range = ranges.stream().findFirst().orElse(fullRange).subrange(maxSize);</span> <span class="fc" id="L349"> final String displayName = file.as(_Displayable_).getDisplayName(); // FIXME: getRdfsLabel()</span> <span class="pc bpc" id="L350" title="1 of 2 branches missed."> final HttpStatus status = range.equals(fullRange) ? OK : PARTIAL_CONTENT;</span> <span class="fc" id="L351"> return file.getContent().map(resource -&gt; ResponseEntity.status(status)</span> <span class="fc" id="L352"> .contentType(new MediaType(&quot;audio&quot;, &quot;mpeg&quot;))</span> <span class="fc" id="L353"> .header(CONTENT_DISPOSITION, contentDisposition(displayName))</span> <span class="fc" id="L354"> .body(range.getRegion(resource)))</span> <span class="fc" id="L355"> .orElseThrow(NotFoundException::new);</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ @Nonnull private ResponseEntity&lt;byte[]&gt; bytesResponse (@Nonnull final byte[] bytes, @Nonnull final String type, @Nonnull final String subtype, @Nonnull final String contentDisposition) { <span class="fc" id="L367"> return ResponseEntity.ok()</span> <span class="fc" id="L368"> .contentType(new MediaType(type, subtype))</span> <span class="fc" id="L369"> .contentLength(bytes.length)</span> <span class="fc" id="L370"> .header(CONTENT_DISPOSITION, contentDisposition(contentDisposition))</span> <span class="fc" id="L371"> .body(bytes);</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ @Nonnull private static String contentDisposition (@Nonnull final String string) { // See https://tools.ietf.org/html/rfc6266#section-5 <span class="fc" id="L381"> return String.format(&quot;filename=\&quot;%s\&quot;; filename*=utf-8''%s&quot;, string, URLEncoder.encode(string, UTF_8));</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ private void checkStatus() { <span class="pc bpc" id="L389" title="1 of 2 branches missed."> if (persistenceInitialized)</span> { <span class="nc" id="L391"> throw new UnavailableException();</span> } <span class="fc" id="L393"> }</span> } </pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.7.202105040129</span></div></body></html>