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 :: REST</a> > <a href="index.source.html" class="el_package">it.tidalwave.bluemarine2.rest.impl</a> > <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 "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.
*
* *********************************************************************************************************************
*
* 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<ENTITY, FINDER extends SourceAwareFinder<FINDER, ENTITY>> extends SourceAwareFinder<ENTITY, FINDER>
{
public Stream<ENTITY> 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("onPersistenceInitializedNotification({})", 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 = "/record", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public List<RecordResource> getRecords (@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L129"> log.info("getRecords({}, {})", 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 = "/record/{id}", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public DetailedRecordResource getRecord (@PathVariable final String id,
@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L150"> log.info("getRecord({}, {}, {})", id, source, fallback);</span>
<span class="fc" id="L151"> checkStatus();</span>
<span class="fc" id="L152"> final List<TrackResource> 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 -> new DetailedRecordResource(record, tracks)));</span>
}
/*******************************************************************************************************************
*
* Exports the cover art of a record.
*
* @param id the record id
* @return the cover art image
*
******************************************************************************************************************/
@RequestMapping(value = "/record/{id}/coverart")
public ResponseEntity<byte[]> getRecordCoverArt (@PathVariable final String id)
{
<span class="fc" id="L168"> log.info("getRecordCoverArt({})", 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 -> track.asMany(_AudioFileSupplier_).stream())</span>
<span class="fc" id="L173"> .map(AudioFileSupplier::getAudioFile)</span>
<span class="fc" id="L174"> .flatMap(af -> af.getMetadata().getAll(ARTWORK).stream())</span>
<span class="fc" id="L175"> .findAny()</span>
<span class="fc" id="L176"> .map(bytes -> bytesResponse(bytes, "image", "jpeg", "coverart.jpg"))</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 = "/track", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public List<TrackResource> getTracks (@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L194"> log.info("getTracks({}, {})", 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 = "/track/{id}", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public TrackResource getTrack (@PathVariable final String id,
@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L215"> log.info("getTrack({}, {}, {})", 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 = "/audiofile", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public List<AudioFileResource> getAudioFiles (@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L234"> log.info("getAudioFiles({}, {})", 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 = "/audiofile/{id}", produces = { APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE })
public AudioFileResource getAudioFile (@PathVariable final String id,
@RequestParam(required = false, defaultValue = "embedded") final String source,
@RequestParam(required = false, defaultValue = "embedded") final String fallback)
{
<span class="fc" id="L255"> log.info("getAudioFile({}, {}, {})", 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 "Range" HTTP header
* @return the binary contents
*
******************************************************************************************************************/
@RequestMapping(value = "/audiofile/{id}/content")
public ResponseEntity<ResourceRegion> getAudioFileContent (
@PathVariable final String id,
@RequestHeader(name = "Range", required = false) final String rangeHeader)
{
<span class="fc" id="L272"> log.info("getAudioFileContent({})", 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 -> 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 = "/audiofile/{id}/coverart")
public ResponseEntity<byte[]> getAudioFileCoverArt (@PathVariable final String id)
{
<span class="fc" id="L288"> log.info("getAudioFileCoverArt({})", id);</span>
<span class="fc" id="L289"> checkStatus();</span>
<span class="fc" id="L290"> final Optional<AudioFile> audioFile = catalog.findAudioFiles().withId(Id.of(id)).optionalResult();</span>
<span class="fc" id="L291"> log.debug(">>>> audioFile: {}", audioFile);</span>
<span class="fc" id="L292"> return audioFile.flatMap(file -> file.getMetadata().getAll(ARTWORK).stream().findFirst())</span>
<span class="fc" id="L293"> .map(bytes -> bytesResponse(bytes, "image", "jpeg", "coverart.jpg"))</span>
<span class="fc" id="L294"> .orElseThrow(NotFoundException::new);</span>
}
/*******************************************************************************************************************
*
******************************************************************************************************************/
@Nonnull
private <T> T single (@Nonnull final List<T> 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 <ENTITY, FINDER extends SourceAwareFinder<ENTITY, FINDER>, JSON>
List<JSON> finalized (@Nonnull final FINDER finder,
@Nonnull final String source,
@Nonnull final String fallback,
@Nonnull final Function<ENTITY, JSON> 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<ENTITY>)f) // FIXME: hacky, because SourceAwareFinder does not extends Finder</span>
<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<ResourceRegion> 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<Range> ranges = Range.fromHeader(rangeHeader, length);</span>
<span class="pc bpc" id="L339" title="1 of 2 branches missed."> if (ranges.size() > 1)</span>
{
<span class="nc" id="L341"> throw new RuntimeException("Can't support multi-range" + ranges); // FIXME</span>
}
// E.g. HTML5 <audio> 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 -> ResponseEntity.status(status)</span>
<span class="fc" id="L352"> .contentType(new MediaType("audio", "mpeg"))</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<byte[]> 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("filename=\"%s\"; filename*=utf-8''%s", 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>