/*
* Copyright 2013-2014 Odysseus Software GmbH
*
* 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.
*/
package org.musicmount.builder.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.musicmount.builder.model.Album;
import org.musicmount.builder.model.Artist;
import org.musicmount.builder.model.ArtistType;
import org.musicmount.builder.model.Disc;
import org.musicmount.builder.model.Playlist;
import org.musicmount.builder.model.Track;
import de.odysseus.staxon.json.JsonXMLConfigBuilder;
import de.odysseus.staxon.json.JsonXMLOutputFactory;
import de.odysseus.staxon.json.JsonXMLStreamConstants;
import de.odysseus.staxon.json.JsonXMLStreamWriter;
import de.odysseus.staxon.xml.util.PrettyXMLStreamWriter;
public abstract class ResponseFormatter<T extends XMLStreamWriter> {
public static class JSON extends ResponseFormatter<JsonXMLStreamWriter> {
private final JsonXMLOutputFactory factory;
public JSON(String apiVersion, LocalStrings localStrings, boolean directoryIndex, boolean includeUnknownGenre, boolean useGrouping, boolean prettyPrint) {
super(apiVersion, localStrings, directoryIndex ? "index.json" : null, includeUnknownGenre, useGrouping);
factory = new JsonXMLOutputFactory(new JsonXMLConfigBuilder().prettyPrint(prettyPrint).virtualRoot("response").build());
}
void writeNumberProperty(JsonXMLStreamWriter writer, String name, Number value) throws XMLStreamException {
writer.writeStartElement(name);
writer.writeNumber(value);
writer.writeEndElement();
}
void writeStartArray(JsonXMLStreamWriter writer) throws XMLStreamException {
writer.writeProcessingInstruction(JsonXMLStreamConstants.MULTIPLE_PI_TARGET);
}
JsonXMLStreamWriter createStreamWriter(OutputStream output) throws XMLStreamException {
return factory.createXMLStreamWriter(output);
}
}
public static class XML extends ResponseFormatter<XMLStreamWriter> {
private final XMLOutputFactory factory;
private final boolean prettyPrint;
public XML(String apiVersion, LocalStrings localStrings, boolean directoryIndex, boolean includeUnknownGenre, boolean useGrouping, boolean prettyPrint) {
super(apiVersion, localStrings, directoryIndex ? "index.xml" : null, includeUnknownGenre, useGrouping);
factory = XMLOutputFactory.newFactory();
this.prettyPrint = prettyPrint;
}
void writeNumberProperty(XMLStreamWriter writer, String name, Number value) throws XMLStreamException {
writeStringProperty(writer, name, value.toString());
}
void writeStartArray(XMLStreamWriter writer) throws XMLStreamException {
// do nothing
}
XMLStreamWriter createStreamWriter(OutputStream output) throws XMLStreamException {
XMLStreamWriter writer = factory.createXMLStreamWriter(output);
if (prettyPrint) {
writer = new PrettyXMLStreamWriter(writer);
}
return writer;
}
}
private final String apiVersion;
private final LocalStrings localStrings;
private final String directoryIndex;
private final boolean includeUnknownGenre;
private final boolean useGrouping;
private final String updateToken = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date());
ResponseFormatter(String apiVersion, LocalStrings localStrings, String directoryIndex, boolean includeUnknownGenre, boolean useGrouping) {
this.apiVersion = apiVersion;
this.localStrings = localStrings;
this.directoryIndex = directoryIndex;
this.includeUnknownGenre = includeUnknownGenre;
this.useGrouping = useGrouping;
}
void writeStringProperty(T writer, String name, String value) throws XMLStreamException {
writer.writeStartElement(name);
writer.writeCharacters(value);
writer.writeEndElement();
}
void startResponse(T writer, String contentElement) throws XMLStreamException {
writer.writeStartDocument();
writer.writeStartElement("response");
writeStringProperty(writer, "apiVersion", apiVersion);
writeStringProperty(writer, "updateToken", updateToken);
writer.writeStartElement(contentElement);
}
void endResponse(T writer) throws XMLStreamException {
writer.writeEndElement(); // <content element>
writer.writeEndElement(); // response
writer.writeEndDocument();
writer.close();
}
abstract void writeNumberProperty(T writer, String name, Number value) throws XMLStreamException;
abstract void writeStartArray(T writer) throws XMLStreamException;
abstract T createStreamWriter(OutputStream output) throws XMLStreamException;
private String getDocumentPath(String path) {
if (directoryIndex == null || path.length() <= directoryIndex.length()) {
return path;
}
int slash = path.length() - directoryIndex.length() - 1;
if (path.charAt(slash) != '/' || !path.endsWith(directoryIndex)) {
return path;
}
return path.substring(0, slash + 1);
}
private List<String> genreList(Playlist playlist) {
// collect genre counts
final Map<String, Integer> map = new HashMap<String, Integer>();
int unknownCount = 0;
for (Track track : playlist.getTracks()) {
String genre = track.getGenre();
if (genre == null) {
unknownCount++;
} else {
Integer count = map.get(genre);
map.put(genre, Integer.valueOf(count == null ? 1 : count + 1));
}
}
if (includeUnknownGenre && unknownCount > 0) {
map.put(localStrings.getUnknownGenre(), unknownCount);
}
// sort decreasing by counts
ArrayList<String> result = new ArrayList<String>(map.keySet());
Collections.sort(result, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int cmp = map.get(o2).compareTo(map.get(o1));
if (cmp == 0) {
cmp = o1.compareTo(o2);
}
return cmp;
}
});
return result;
}
private List<String> genreList(Artist artist) {
Playlist playlist = new Playlist();
for (Album album : artist.albums()) {
if (artist.getArtistType() == ArtistType.TrackArtist) {
for (Track track : album.getTracks()) {
if (track.getArtist() == artist) {
playlist.getTracks().add(track);
}
}
} else { // take all tracks into account
playlist.getTracks().addAll(album.getTracks());
}
}
return genreList(playlist);
}
private String getDefaultArtistTitle(ArtistType artistType) {
return artistType == ArtistType.AlbumArtist ? localStrings.getVariousArtists() : localStrings.getUnknownArtist();
}
private String getDefaultAlbumTitle() {
return localStrings.getUnknownAlbum();
}
private String getDefaultTrackTitle() {
return localStrings.getUnknownTrack();
}
public Integer albumYear(Album album, Integer defaultValue) {
Integer maximumYear = null;
for (Track track : album.getTracks()) {
if (maximumYear == null || track.getYear() != null && maximumYear.compareTo(track.getYear()) < 0) {
maximumYear = track.getYear();
}
}
return maximumYear != null ? maximumYear : defaultValue;
}
private void formatArtistSections(T writer, Iterable<CollectionSection<Artist>> sections, ResourceLocator resourceLocator, ImageType imageType, ArtistType artistType, Map<Artist, Album> representativeAlbums) throws IOException, XMLStreamException {
writeStartArray(writer);
for (CollectionSection<Artist> section : sections) {
writer.writeStartElement("section");
if (section.getTitle() != null) {
writeStringProperty(writer, "title", section.getTitle());
}
writeStartArray(writer);
for (Artist item : section.getItems()) {
writer.writeStartElement("item");
writeStringProperty(writer, "title", item.getTitle() == null ? getDefaultArtistTitle(artistType) : item.getTitle());
Album representativeAlbum = representativeAlbums != null ? representativeAlbums.get(item) : null;
String imagePath = representativeAlbum != null ? resourceLocator.getAlbumImagePath(representativeAlbum, imageType) : null;
if (imagePath != null) {
writeStringProperty(writer, "imagePath", imagePath);
}
writeStringProperty(writer, "albumCollectionPath", getDocumentPath(resourceLocator.getAlbumCollectionPath(item)));
List<String> genreList = genreList(item);
if (genreList.size() > 0) {
writeStartArray(writer);
for (String genre : genreList) {
writeStringProperty(writer, "genre", genre);
}
}
writeNumberProperty(writer, "albumCount", item.albumsCount());
writer.writeEndElement();
}
writer.writeEndElement();
}
}
private void formatAlbumSections(T writer, Iterable<CollectionSection<Album>> sections, ResourceLocator resourceLocator, ImageType imageType, boolean writeCompilationInfo) throws IOException, XMLStreamException {
writeStartArray(writer);
for (CollectionSection<Album> section : sections) {
writer.writeStartElement("section");
if (section.getTitle() != null) {
writeStringProperty(writer, "title", section.getTitle());
}
writeStartArray(writer);
for (Album item : section.getItems()) {
writer.writeStartElement("item");
writeStringProperty(writer, "title", item.getTitle() == null ? getDefaultAlbumTitle() : item.getTitle());
String imagePath = resourceLocator.getAlbumImagePath(item, imageType);
if (imagePath != null) {
writeStringProperty(writer, "imagePath", imagePath);
}
if (writeCompilationInfo && item.isCompilation() && item.getArtist().getTitle() != null) {
writeStringProperty(writer, "info", localStrings.getCompilation());
}
writeStringProperty(writer, "artist", item.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.AlbumArtist) : item.getArtist().getTitle());
writeStringProperty(writer, "albumPath", getDocumentPath(resourceLocator.getAlbumPath(item)));
List<String> genreList = genreList(item);
if (genreList.size() > 0) {
writeStartArray(writer);
for (String genre : genreList(item)) {
writeStringProperty(writer, "genre", genre);
}
}
Integer year = albumYear(item, null);
if (year != null) {
writeNumberProperty(writer, "year", year);
}
writer.writeEndElement();
}
writer.writeEndElement();
}
}
private void formatTrackSections(T writer, Iterable<CollectionSection<Track>> sections, ResourceLocator resourceLocator, AssetLocator assetLocator, ImageType imageType) throws IOException, XMLStreamException {
writeStartArray(writer);
for (CollectionSection<Track> section : sections) {
writer.writeStartElement("section");
if (section.getTitle() != null) {
writeStringProperty(writer, "title", section.getTitle());
}
writeStartArray(writer);
for (Track item : section.getItems()) {
writer.writeStartElement("item");
writeStringProperty(writer, "title", item.getTitle() == null ? getDefaultTrackTitle() : item.getTitle());
writeStringProperty(writer, "artist", item.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.TrackArtist) : item.getArtist().getTitle());
if (item.getGenre() != null || includeUnknownGenre) {
writeStringProperty(writer, "genre", item.getGenre() != null ? item.getGenre() : localStrings.getUnknownGenre());
}
if (item.getDuration() != null) {
writeNumberProperty(writer, "duration", item.getDuration());
}
if (item.getAlbum() != null) {
String imagePath = resourceLocator.getAlbumImagePath(item.getAlbum(), imageType);
if (imagePath != null) {
writeStringProperty(writer, "imagePath", imagePath);
}
writeStringProperty(writer, "albumPath", getDocumentPath(resourceLocator.getAlbumPath(item.getAlbum())));
}
if (assetLocator != null) {
String assetPath = assetLocator.getAssetPath(item.getResource());
if (assetPath != null) {
writeStringProperty(writer, "assetPath", assetPath);
}
}
writer.writeEndElement();
}
writer.writeEndElement();
}
}
private Iterable<CollectionSection<Album>> createAlbumCollectionSections(Artist artist) {
// split albums into sections with regular albums and others (compilations/various/unknown)
CollectionSection<Album> regularAlbums = new CollectionSection<Album>(localStrings.getRegularAlbumSection());
CollectionSection<Album> otherAlbums = new CollectionSection<Album>(artist.getTitle() != null ?localStrings.getCompilationAlbumSection() : null);
for (Album album : artist.albums()) {
if (artist.getTitle() != null) {
if (album.isCompilation()) {
otherAlbums.getItems().add(album);
} else {
regularAlbums.getItems().add(album);
}
} else {
otherAlbums.getItems().add(album);
}
}
Collection<CollectionSection<Album>> sections = new ArrayList<CollectionSection<Album>>();
final Comparator<Album> titleComparator = new TitledComparator<Album>(localStrings, getDefaultAlbumTitle(), null);
if (!regularAlbums.getItems().isEmpty()) {
// sort regular albums by year and title
Collections.sort(regularAlbums.getItems(), new Comparator<Album>() {
@Override
public int compare(Album item1, Album item2) {
int result = albumYear(item1, Integer.MAX_VALUE).compareTo(albumYear(item2, Integer.MAX_VALUE));
if (result != 0) {
return result;
}
return titleComparator.compare(item1, item2);
}
});
sections.add(regularAlbums);
}
if (!otherAlbums.getItems().isEmpty()) {
// sort other albums by title only
Collections.sort(otherAlbums.getItems(), titleComparator);
sections.add(otherAlbums);
}
return sections;
}
public void formatServiceIndex(ResourceLocator resourceLocator, OutputStream output) throws IOException, XMLStreamException {
T writer = createStreamWriter(output);
startResponse(writer, "serviceIndex");
String albumArtistIndexPath = resourceLocator.getArtistIndexPath(ArtistType.AlbumArtist);
if (albumArtistIndexPath != null) {
writeStringProperty(writer, "albumArtistIndexPath", getDocumentPath(albumArtistIndexPath));
}
String trackArtistIndexPath = resourceLocator.getArtistIndexPath(ArtistType.TrackArtist);
if (trackArtistIndexPath != null) {
writeStringProperty(writer, "artistIndexPath", getDocumentPath(trackArtistIndexPath));
}
String albumIndexPath = resourceLocator.getAlbumIndexPath();
if (albumIndexPath != null) {
writeStringProperty(writer, "albumIndexPath", getDocumentPath(albumIndexPath));
}
String trackIndexPath = resourceLocator.getTrackIndexPath();
if (trackIndexPath != null) {
writeStringProperty(writer, "trackIndexPath", getDocumentPath(trackIndexPath));
}
endResponse(writer);
}
public void formatArtistIndex(Iterable<? extends Artist> artists, ArtistType artistType, OutputStream output, ResourceLocator resourceLocator, Map<Artist, Album> representativeAlbums) throws IOException, XMLStreamException {
T writer = createStreamWriter(output);
startResponse(writer, "artistCollection");
writeStringProperty(writer, "title", localStrings.getArtistIndexTitle(artistType));
TitledComparator<Artist> comparator = new TitledComparator<Artist>(localStrings, getDefaultArtistTitle(artistType), new Comparator<Artist>() {
@Override
public int compare(Artist o1, Artist o2) { // sort equally titled artists descending by album count
return -Integer.valueOf(o1.albumsCount()).compareTo(Integer.valueOf(o2.albumsCount()));
}
});
Iterable<CollectionSection<Artist>> sections = CollectionSection.createIndex(artists, comparator);
formatArtistSections(writer, sections, resourceLocator, ImageType.Thumbnail, artistType, representativeAlbums);
endResponse(writer);
}
public void formatAlbumIndex(Iterable<Album> albums, OutputStream output, ResourceLocator resourceLocator) throws IOException, XMLStreamException {
T writer = createStreamWriter(output);
startResponse(writer, "albumCollection");
writeStringProperty(writer, "title", "Albums");
TitledComparator<Album> comparator = new TitledComparator<Album>(localStrings, getDefaultAlbumTitle(), new Comparator<Album>() {
@Override
public int compare(Album o1, Album o2) { // sort equally titled albums by album artist
String title1 = o1.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.AlbumArtist) : o1.getArtist().getTitle();
String title2 = o2.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.AlbumArtist) : o2.getArtist().getTitle();
return title1.compareTo(title2);
}
});
Iterable<CollectionSection<Album>> sections = CollectionSection.createIndex(albums, comparator);
formatAlbumSections(writer, sections, resourceLocator, ImageType.Thumbnail, true);
endResponse(writer);
}
public void formatTrackIndex(Iterable<Track> tracks, OutputStream output, ResourceLocator resourceLocator, AssetLocator assetLocator) throws IOException, XMLStreamException {
T writer = createStreamWriter(output);
startResponse(writer, "trackCollection");
writeStringProperty(writer, "title", "Tracks");
TitledComparator<Track> comparator = new TitledComparator<Track>(localStrings, getDefaultTrackTitle(), new Comparator<Track>() {
@Override
public int compare(Track o1, Track o2) { // sort equally titled tracks by artist
String title1 = o1.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.TrackArtist) : o1.getArtist().getTitle();
String title2 = o2.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.TrackArtist) : o2.getArtist().getTitle();
return title1.compareTo(title2);
}
});
Iterable<CollectionSection<Track>> sections = CollectionSection.createIndex(tracks, comparator);
formatTrackSections(writer, sections, resourceLocator, assetLocator, ImageType.Thumbnail);
endResponse(writer);
}
public Album formatAlbumCollection(Artist artist, OutputStream output, ResourceLocator resourceLocator) throws IOException, XMLStreamException {
String title = artist.getTitle() == null ? getDefaultArtistTitle(artist.getArtistType()) : artist.getTitle();
Iterable<CollectionSection<Album>> sections = createAlbumCollectionSections(artist);
T writer = createStreamWriter(output);
startResponse(writer, "albumCollection");
writeStringProperty(writer, "title", title);
formatAlbumSections(writer, sections, resourceLocator, ImageType.Tile, false);
endResponse(writer);
// answer first album with an associated artist
for (CollectionSection<Album> section : sections) {
for (Album album : section.getItems()) {
if (album.getArtist().getTitle() != null) {
return album;
}
}
}
return null; // no representative album for unknown/various artist
}
public void formatAlbum(Album album, OutputStream output, ResourceLocator resourceLocator, AssetLocator assetLocator) throws IOException, XMLStreamException {
T writer = createStreamWriter(output);
startResponse(writer, "album");
writeStringProperty(writer, "title", album.getTitle() == null ? getDefaultAlbumTitle() : album.getTitle());
if (album.isCompilation() && album.getArtist().getTitle() != null) {
writeStringProperty(writer, "info", localStrings.getCompilation());
}
writeStringProperty(writer, "artist", album.getArtist().getTitle() == null ? getDefaultArtistTitle(ArtistType.AlbumArtist) : album.getArtist().getTitle());
// writeStringProperty(writer, "albumPath", resourceLocator.getAlbumPath(album));
List<String> genreList = genreList(album);
if (genreList.size() > 0) {
writeStartArray(writer);
for (String genre : genreList(album)) {
writeStringProperty(writer, "genre", genre);
}
}
Integer year = albumYear(album, null);
if (year != null) {
writeNumberProperty(writer, "year", year);
}
String albumImagePath = resourceLocator.getAlbumImagePath(album, ImageType.Artwork);
if (albumImagePath != null) {
writeStringProperty(writer, "imagePath", albumImagePath);
}
/*
* format tracks
*/
String trackImagePath = resourceLocator.getAlbumImagePath(album, ImageType.Thumbnail);
writer.writeStartElement("trackCollection");
writeStringProperty(writer, "title", "Tracks");
writeStartArray(writer);
for (Disc disc : album.getDiscs().values()) {
writer.writeStartElement("section");
if (disc.getDiscNumber() == 0 || disc.getDiscNumber() == 1 && album.getDiscs().size() == 1) {
writeStringProperty(writer, "title", localStrings.getTracks());
} else {
writeStringProperty(writer, "title", String.format("%s %d", localStrings.getDisc(), disc.getDiscNumber()));
}
ArrayList<Track> items = new ArrayList<Track>(disc.getTracks());
Collections.sort(items, new Comparator<Track>() {
public int compare(Track t1, Track t2) {
if (t1.getTrackNumber() == null && t2.getTrackNumber() == null) {
return 0;
} else if (t1.getTrackNumber() == null) {
return -1;
} else if (t2.getTrackNumber() == null) {
return +1;
}
return t1.getTrackNumber().compareTo(t2.getTrackNumber());
}
});
writeStartArray(writer);
for (Track item : items) {
writer.writeStartElement("item");
writeStringProperty(writer, "title", item.getTitle() == null ? getDefaultTrackTitle() : item.getTitle());
String artist = item.getArtist().getTitle();
if (artist == null) {
artist = localStrings.getUnknownArtist();
}
writeStringProperty(writer, "artist", artist);
if (trackImagePath != null) {
writeStringProperty(writer, "imagePath", trackImagePath);
}
if (item.getGenre() != null) {
writeStringProperty(writer, "genre", item.getGenre());
}
if (item.getGrouping() != null && useGrouping) {
writeStringProperty(writer, "grouping", item.getGrouping());
}
if (item.getComposer() != null) {
writeStringProperty(writer, "composer", item.getComposer());
}
if (item.getTrackNumber() != null) {
writeNumberProperty(writer, "trackNumber", item.getTrackNumber());
}
if (item.getDuration() != null) {
writeNumberProperty(writer, "duration", item.getDuration());
}
String assetPath = assetLocator.getAssetPath(item.getResource());
if (assetPath != null) {
writeStringProperty(writer, "assetPath", assetPath);
}
writer.writeEndElement();
}
writer.writeEndElement(); // section
}
writer.writeEndElement(); // trackCollection
endResponse(writer);
}
}