package me.moodcat.api;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.moodcat.api.filters.AwardPoints;
import me.moodcat.api.models.SongModel;
import me.moodcat.database.controllers.ClassificationDAO;
import me.moodcat.database.controllers.SongDAO;
import me.moodcat.database.embeddables.VAVector;
import me.moodcat.database.entities.Classification;
import me.moodcat.database.entities.Song;
import me.moodcat.database.entities.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.google.inject.persist.Transactional;
/**
* The {@code ArtistAPI} is an API entry point to do CRUD operations to {@link Song} entities.
*/
@Path("/api/songs")
@Produces(MediaType.APPLICATION_JSON)
public class SongAPI {
/**
* The points a user gains when he classifies a song.
*/
protected static final int CLASSIFICATION_POINTS_AWARD = 6;
private static final double VECTOR_DELTA = 1e-2;
/**
* The number of songs retrieved for each classification list.
*/
private static final int NUMBER_OF_CLASSIFICATION_SONGS = 5;
/**
* The weight used to modify {@link VAVector vectors} of {@link Song songs} with a
* classification vector.
*/
private static final double CLASSIFICATION_WEIGHT = 0.01;
/**
* The accepted valence and arousal values.
*/
private static final Double[] ACCEPTED_DIMENSION_VALUES = new Double[] {
-1.0, -0.5, 0.0, 0.5, 1.0
};
/**
* Java facade to talk to the database to obtain songs.
*/
private final SongDAO songDAO;
private final ClassificationDAO classificationDAO;
private final Provider<User> currentUserProvider;
@Inject
@VisibleForTesting
public SongAPI(final SongDAO songDAO, final ClassificationDAO classificationDAO,
@Named("current.user") final Provider<User> currentUserProvider) {
this.songDAO = songDAO;
this.classificationDAO = classificationDAO;
this.currentUserProvider = currentUserProvider;
}
@GET
@Transactional
public List<SongModel> getSongs() {
return transformSongs(songDAO.listSongs());
}
@GET
@Path("{id}")
@Transactional
public SongModel getSongById(@PathParam("id") final int id) {
return SongModel.transform(songDAO.findById(id));
}
@GET
@Path("toclassify")
@Transactional
public List<SongModel> toClassify() {
return transformSongs(songDAO.listRandomsongs(NUMBER_OF_CLASSIFICATION_SONGS));
}
private List<SongModel> transformSongs(final List<Song> songs) {
return songs.stream()
.map(SongModel::transform)
.collect(Collectors.toList());
}
/*
* TODO Method currently checks if a classification already exists in the database.
* This should not happen, because after the initial classification
* the song will not be available for classification (and this endpoint
* should throw an IllegalArgumentException instead). However, this
* endpoint is currently also abused for the classification game.
* In order to prevent users to abuse this to gain points with fake
* classifications on the same song, we ignore duplicate classifications.
*/
/**
* Process a user classification for the given songId.
*
* @param id
* The id of the song.
* @param classification
* The classification of the user for the song.
* @return The classification provided.
* @throws InvalidClassificationException
* When the classification provided was invalid
*/
@POST
@Path("{id}/classify")
@Transactional
@AwardPoints(CLASSIFICATION_POINTS_AWARD)
public ClassificationRequest classifySong(@PathParam("id") final int id,
final ClassificationRequest classification)
throws InvalidClassificationException {
final User user = this.currentUserProvider.get();
final Song song = this.songDAO.findBySoundCloudId(id);
final VAVector classificationVector = new VAVector(
classification.getValence(),
classification.getArousal());
if (classificationDAO.exists(user, song)) {
throw new IllegalArgumentException("Already classified this song");
}
assertDimensionIsValid(classification.getValence());
assertDimensionIsValid(classification.getArousal());
updateVectorFromClassification(user, song, classificationVector);
return classification;
}
private void updateVectorFromClassification(final User user, final Song song,
final VAVector classificationVector) {
final VAVector vector;
if (song.getValenceArousal().distance(VAVector.ZERO) < VECTOR_DELTA) {
// If near origin set the vector
vector = classificationVector;
} else {
// otherwise adjust the vector
vector = adjustSongVector(classificationVector, song);
}
song.setValenceArousal(vector);
this.persistClassification(user, song, classificationVector);
this.songDAO.merge(song);
}
private void persistClassification(final User user, final Song song,
final VAVector classificationVector) {
final Classification classificationEntity = new Classification();
classificationEntity.setValenceArousal(classificationVector);
classificationEntity.setSong(song);
classificationEntity.setUser(user);
this.classificationDAO.persist(classificationEntity);
}
/**
* Process a user classification game for the given songId.
*
* @param id
* The id of the song.
* @param classification
* The classification of the user for the song.
* @return The classification provided.
* @throws InvalidClassificationException
* When the classification provided was invalid
*/
@POST
@Path("{id}/classifygame")
@Transactional
@AwardPoints(CLASSIFICATION_POINTS_AWARD)
public ClassificationRequest approachSong(@PathParam("id") final int id,
final ClassificationRequest classification)
throws InvalidClassificationException {
final Song song = this.songDAO.findBySoundCloudId(id);
assertDimensionIsValid(classification.getValence());
assertDimensionIsValid(classification.getArousal());
song.setValenceArousal(new VAVector(classification.getValence(), classification
.getArousal()));
songDAO.merge(song);
return classification;
}
private void assertDimensionIsValid(final double value) throws InvalidClassificationException {
if (!Arrays.asList(ACCEPTED_DIMENSION_VALUES).contains(value)) {
throw new InvalidClassificationException();
}
}
private VAVector adjustSongVector(final VAVector classificationVector, final Song song) {
final VAVector songVector = song.getValenceArousal();
final VAVector scaledDistance = classificationVector.subtract(songVector)
.multiply(CLASSIFICATION_WEIGHT);
return songVector.add(scaledDistance);
}
/**
* ClassificationRequest with the arousal and valence the user would classify the specific song
* for.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ClassificationRequest {
/**
* The valence for the song.
*
* @param valence
* The valence to set.
* @return The valence that was classified.
*/
private double valence;
/**
* The arousal for the song.
*
* @param arousal
* The arousal to set.
* @return The arousal that was classified.
*/
private double arousal;
}
/**
* Thrown if the classification was invalid.
*/
protected static class InvalidClassificationException extends IllegalArgumentException {
/**
* Generated ID.
*/
private static final long serialVersionUID = -6684926632173744801L;
}
}