package com.pugh.sockso.web.action;
import com.pugh.sockso.cache.CacheException;
import com.pugh.sockso.cache.ObjectCache;
import com.pugh.sockso.Constants;
import com.pugh.sockso.web.*;
import com.pugh.sockso.Utils;
import com.pugh.sockso.db.Database;
import com.pugh.sockso.resources.Locale;
import com.pugh.sockso.music.CollectionManager;
import com.pugh.sockso.music.MusicSearch;
import com.pugh.sockso.music.Track;
import com.pugh.sockso.music.Artist;
import com.pugh.sockso.templates.json.TSearch;
import com.pugh.sockso.templates.json.TString;
import com.pugh.sockso.templates.json.TFolders;
import com.pugh.sockso.templates.json.TResolvePath;
import com.pugh.sockso.templates.json.TTracks;
import com.pugh.sockso.templates.json.TTracksForPath;
import com.pugh.sockso.templates.json.TSimilarArtists;
import com.pugh.sockso.templates.json.TServerInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Arrays;
import java.util.Comparator;
import org.apache.log4j.Logger;
import com.google.inject.Inject;
public class Jsoner extends BaseAction {
private static final Logger log = Logger.getLogger( Jsoner.class );
private final CollectionManager cm;
private final ObjectCache cache;
@Inject
public Jsoner( final CollectionManager cm, final ObjectCache cache ) {
this.cm = cm;
this.cache = cache;
}
/**
* handles the json command which generates json documents
*
* @param res the response object
* @param args the command arguments
* @param user current user
*
* @throws SQLException
* @throws IOException
* @throws BadRequestException
*
*/
public void handleRequest() throws SQLException, IOException, BadRequestException, CacheException {
final Request req = getRequest();
final String type = req.getUrlParam( 1 );
if ( type.equals("search") )
search();
else if ( type.equals("savePlaylist") )
savePlaylist();
else if ( type.equals("deletePlaylist") )
deletePlaylist();
else if ( type.equals("folder") )
folder();
else if ( type.equals("resolvePath") )
resolvePath();
else if ( type.equals("tracksForPath") )
tracksForPath();
else if ( type.equals("similarArtists") )
similarArtists();
else if ( type.equals("tracks") )
tracks();
else if ( type.equals("serverinfo") )
serverinfo();
else throw new BadRequestException( "Unknown json request (" + type + ")", 400 );
}
/**
* given some url arguments (ar123, al456, etc...) queries for the tracks
* that belong to these items
*
* @throws java.io.IOException
* @throws java.sql.SQLException
* @throws com.pugh.sockso.web.BadRequestException
*
*/
protected void tracks() throws IOException, SQLException, BadRequestException {
final Request req = getRequest();
final List<Track> tracks = Track.getTracksFromPlayArgs( getDatabase(), req.getPlayParams(true) );
showTracks( tracks );
}
/**
* shows the tracks specified
*
* @param tracks
*
* @throws java.io.IOException
*
*/
protected void showTracks( final List<Track> tracks ) throws IOException {
final TTracks tpl = new TTracks();
tpl.setTracks( tracks );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* queries audioscrobbler for artists similar to the one specified, and then
* check against our artists to see which ones we have which are similar
*
* @throws BadRequestException
* @throws SQLException
* @throws IOException
*
*/
protected void similarArtists() throws BadRequestException, SQLException, IOException, CacheException {
final AudioScrobbler scrobbler = new AudioScrobbler( getDatabase(), cache );
final RelatedArtists related = new RelatedArtists( getDatabase(), scrobbler );
final Request req = getRequest();
final int artistId = Integer.parseInt( req.getUrlParam(2) );
showSimilarArtists( related.getRelatedArtistsFor(artistId) );
}
/**
* shows the specified artists
*
* @param artists
*
*/
protected void showSimilarArtists( final List<Artist> artists ) throws IOException {
final TSimilarArtists tpl = new TSimilarArtists();
tpl.setArtists( artists );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* tries to delete a users playlist. needs to check things like did they
* create it, etc... if all goes ok then sends back the ID so that the
* javascript handler can do whatever...
*
* @throws BadRequestException
* @throws SQLException
* @throws IOException
*
*/
protected void deletePlaylist() throws BadRequestException, SQLException, IOException {
final Request req = getRequest();
final User user = getUser();
final Locale locale = getLocale();
if ( user == null ) throw new BadRequestException( locale.getString("www.json.error.notLoggedIn"), 403 );
final Database db = getDatabase();
final int id = Integer.parseInt( req.getUrlParam(2) );
final String sql = " select 1 " +
" from playlists p " +
" where p.id = ? " +
" and p.user_id = ? ";
ResultSet rs = null;
PreparedStatement st = null;
try {
// check user owns playlist before deleting it
st = db.prepare( sql );
st.setInt( 1, id );
st.setInt( 2, user.getId() );
rs = st.executeQuery();
if ( !rs.next() )
throw new BadRequestException( "You don't own that playlist", 403 );
cm.removePlaylist( id );
// done, send success response
final TString tpl = new TString();
tpl.setResult( Integer.toString(id) );
getResponse().showJson( tpl.makeRenderer() );
}
finally {
Utils.close( rs );
Utils.close( st );
}
}
/**
* this method outputs the tracks that lie below a given path
*
* @throws com.pugh.sockso.web.BadRequestException
* @throws java.sql.SQLException
* @throws java.io.IOException
*
*/
protected void tracksForPath() throws BadRequestException, SQLException, IOException {
Utils.checkFeatureEnabled( getProperties(), Constants.WWW_BROWSE_FOLDERS_ENABLED );
ResultSet rs = null;
PreparedStatement st = null;
try {
final Database db = getDatabase();
final Request req = getRequest();
final String path = req.getArgument( "path" );
final TTracksForPath tpl = new TTracksForPath();
tpl.setTracks( Track.getTracksFromPath(db,path) );
getResponse().showJson( tpl.makeRenderer() );
}
finally {
Utils.close( rs );
Utils.close( st );
}
}
/**
* this action allows you to give the path of a track on disk, and it will
* be resolved to the tracks internal ID. this can then be used normally
* for playing music. the path coming in is assumed to have forward slashes
* to delimit path components, but this needs to be converted to whatever
* the actual path separator is for the current system BEFORE we try and
* query the database, otherwise, well, it just won't work.
*
* NB! ATM, this feature is only here for the the folder browsing stuff,
* so if that's not turned on this this won't work.
*
*
*/
protected void resolvePath() throws BadRequestException, SQLException, IOException {
// check folder browsing is enabled
Utils.checkFeatureEnabled( getProperties(), "browse.folders.enabled" );
ResultSet rs = null;
PreparedStatement st = null;
try {
final Database db = getDatabase();
final Locale locale = getLocale();
final Request req = getRequest();
final String path = convertPath( req.getArgument("path") );
final String sql = Track.getSelectFromSql() +
" where t.path = ? ";
st = db.prepare( sql );
st.setString( 1, path );
rs = st.executeQuery();
if ( !rs.next() )
throw new BadRequestException( locale.getString("www.error.trackNotFound"), 404 );
final Track track = Track.createFromResultSet( rs );
final TResolvePath tpl = new TResolvePath();
tpl.setTrack( track );
getResponse().showJson( tpl.makeRenderer() );
}
finally {
Utils.close( rs );
Utils.close( st );
}
}
/**
* converts a path with / as the path separator to be correct for the
* current system.
*
* @param path
* @return
*
*/
protected static String convertPath( final String path ) {
final String separator = System.getProperty( "file.separator" );
return path.replaceAll( "\\/", "\\" + separator );
}
/**
* this method extracts the full path in the request where the relative
* path after the json action name is prefixed by the collection path
* specified in the query string.
*
* e.g. /json/action/File/System/Path?collectionId=2
*
* Will return /home/rod/File/System/Path because the collection with id = 2
* is rooted at /home/rod
*
* @return String
*
*/
private String getPathFromRequest() throws SQLException, BadRequestException {
final Request req = getRequest();
final Locale locale = getLocale();
final int collectionId = Integer.parseInt( req.getArgument("collectionId") );
String path = "";
ResultSet rs = null;
PreparedStatement st = null;
for ( int i=2; i<req.getParamCount(); i++ ) {
final String pathElement = req.getUrlParam( i );
// don't allow going up directories
if ( !pathElement.equals("..") )
path += "/" + req.getUrlParam( i );
}
try {
final Database db = getDatabase();
final String sql = " select c.path " +
" from collection c " +
" where c.id = ? ";
st = db.prepare( sql );
st.setInt( 1, collectionId );
rs = st.executeQuery();
// check the collection exists and we got it's root path
if ( rs.next() ) {
// we need to trim the trailing slash off the collection path
final String collPath = rs.getString( "path" );
path = collPath.substring(0,collPath.length()-1) + path;
}
else
throw new BadRequestException( locale.getString("www.error.invalidCollectionId"), 404 );
path = path.replaceAll( "\\/\\/", "\\/" );
}
finally {
Utils.close( rs );
Utils.close( st );
}
log.debug( "pathFromRequest: " +path );
return path;
}
/**
* if folder browsing is enabled, returns the contents of a folder, otherwise
* it'll throw a BadRequestException
*
*/
protected void folder() throws BadRequestException, SQLException, IOException {
// check folder browsing is enabled
Utils.checkFeatureEnabled( getProperties(), "browse.folders.enabled" );
final String path = getPathFromRequest();
final File folder = new File( path );
log.debug( "Path: " +path );
// check the folder really exists on disk
if ( !folder.exists() )
throw new BadRequestException( "www.error.folderDoesntExist", 404 );
final TFolders tpl = new TFolders();
tpl.setFiles( getOrderedFiles(folder.listFiles()) );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* Takes an array of files, and returns another array sorted by ascending filename
*
* @param contents
*
* @return
*
*/
protected File[] getOrderedFiles( final File[] contents ) {
final File[] toSort = contents.clone();
Arrays.<File>sort( toSort, new Comparator<File>() {
public int compare( final File file1, final File file2 ) {
return file1.getName().compareTo( file2.getName() );
}
});
return toSort;
}
/**
* saves a playlist to the database for the current user. outputs
* a single integer which is the playlist ID if all goes well, otherwise
* you'll get a description of the problem.
*
* @throws IOException
*
*/
protected void savePlaylist() throws IOException, SQLException, BadRequestException {
final Request req = getRequest();
final User user = getUser();
final Locale locale = getLocale();
final String name = req.getUrlParam( 2 ).trim();
final String[] args = req.getPlayParams( 2 );
String result = locale.getString("www.json.error.unknown");
// make sure data is ok first
if ( name.equals("") )
result = locale.getString("www.json.error.noName");
else if ( args.length == 0 )
result = locale.getString("www.json.error.noArguments");
else if ( user == null )
result = locale.getString("www.json.error.notLoggedIn");
else {
final Database db = getDatabase();
final List<Track> vTracks = Track.getTracksFromPlayArgs( db, args );
final Track[] tracks = new Track[ vTracks.size() ];
for ( int i=0; i<vTracks.size(); i++ )
tracks[i] = vTracks.get( i );
result = Integer.toString(
cm.savePlaylist( name, tracks, user )
);
}
final TString tpl = new TString();
tpl.setResult( result );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* performs a search on the music collection for the specified string and
* then creates a json results page
*
* @throws SQLException
* @throws IOException
*
*/
protected void search() throws SQLException, IOException {
final MusicSearch musicSearch = new MusicSearch( getDatabase() );
final Request req = getRequest();
final String query = req.getUrlParam( 2 );
final TSearch tpl = new TSearch();
tpl.setItems( musicSearch.search(query) );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* Returns information about this server (nothing secret)
*
*/
protected void serverinfo() throws IOException {
final TServerInfo tpl = new TServerInfo();
tpl.setProperties( getProperties() );
getResponse().showJson( tpl.makeRenderer() );
}
/**
* Login is not required when requesting serverinfo
*
* @return
*
*/
@Override
public boolean requiresLogin() {
return !getRequest().getUrlParam( 1 )
.equals( "serverinfo" );
}
}