fr.msch.wissl.server.REST.java Source code

Java tutorial

Introduction

Here is the source code for fr.msch.wissl.server.REST.java

Source

/* This file is part of Wissl - Copyright (C) 2013 Mathieu Schnoor
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.msch.wissl.server;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.io.FileUtils;
import org.codehaus.jettison.json.JSONObject;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.util.GenericType;

import fr.msch.wissl.common.Config;
import fr.msch.wissl.server.exception.ForbiddenException;
import fr.msch.wissl.server.exception.SecurityError;

/**
 * RESTful HTTP frontend using JBoss RESTEASY
 * 
 * 
 * @author mathieu.schnoor@gmail.com
 * 
 */
@Path("/")
@Produces("application/json;charset=UTF-8")
public final class REST {

    @Context
    private HttpServletRequest request;

    @Context
    private HttpServletResponse response;

    @HeaderParam("sessionId")
    private String sessionIdHeader;

    @HeaderParam("User-Agent")
    private String userAgent;

    @QueryParam("sessionId")
    private String sessionIdGet;

    private void log(Session session, long nanoStartTime) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getMethod());
        sb.append(' ');
        sb.append(request.getRequestURI());
        sb.append(' ');
        sb.append(session.getUserName());
        sb.append('@');
        sb.append(request.getRemoteAddr());
        sb.append(' ');
        int millis = (int) ((System.nanoTime() - nanoStartTime) / 1000000f);
        sb.append(millis);
        sb.append("ms");
        Logger.info(sb.toString());
    }

    private void nocache() {
        this.response.setHeader("Cache-Control", "no-cache");
    }

    @POST
    @Path("login")
    public String login(@FormParam("username") String username, @FormParam("password") String password)
            throws SQLException, SecurityError {
        long t = System.nanoTime();

        User user = DB.get().getUser(username);
        if (user == null) {
            throw new SecurityError("Invalid username or password");
        }

        user.password = password.getBytes();
        boolean passwordOk = user.checkPassword();
        user.password = null;

        if (!passwordOk) {
            throw new SecurityError("Invalid username or password");
        }

        Session newSession = Session.create(request.getRemoteAddr(), user.id, username, userAgent);

        StringBuilder ret = new StringBuilder();
        ret.append("{ \"sessionId\":\"");
        ret.append(newSession.getSessionId().toString());
        ret.append("\", \"userId\": ");
        ret.append(user.id);
        ret.append(",\"auth\":");
        ret.append(user.auth);
        ret.append("}");

        nocache();
        log(newSession, t);
        return ret.toString();
    }

    @POST
    @Path("logout")
    public void logout() throws SecurityError {
        long t = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        Session.remove(sess);

        nocache();
        log(sess, t);
    }

    @GET
    @Path("users")
    public String getUsers() throws SQLException, SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = sess.getUserId();

        List<User> users = DB.get().getUsers();
        Map<UUID, Session> sessions = Session.getSessions();
        boolean admin = false;

        StringBuilder ret = new StringBuilder();
        ret.append("{ \"users\": [");
        for (Iterator<User> it = users.iterator(); it.hasNext();) {
            User u = it.next();
            ret.append(u.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
            if (u.id == uid) {
                admin = (u.auth == 1);
            }
        }
        ret.append("], \"sessions\": [");
        for (Iterator<Session> it = sessions.values().iterator(); it.hasNext();) {
            Session s = it.next();
            ret.append(s.toJSON(admin));
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, l1);
        return ret.toString();
    }

    @GET
    @Path("hasusers")
    public String hasUsers() throws SQLException {
        boolean hasUsers = DB.get().hasUsers();
        return "{\"hasusers\":" + hasUsers + "}";
    }

    @GET
    @Path("user/{user_id}")
    public String getUser(@PathParam("user_id") int userId) throws SQLException, SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        StringBuilder ret = new StringBuilder();

        User u = DB.get().getUser(userId);
        if (u == null) {
            throw new NotFoundException("Unknown user: " + userId);
        }

        List<Session> sessions = Session.getSessions(userId);

        ret.append("{ \"user\":");
        ret.append(u.toJSON());
        ret.append(",\"sessions\":[");

        for (Iterator<Session> it = sessions.iterator(); it.hasNext();) {
            Session s = it.next();
            ret.append(s.toJSON(u.auth == 1));
            if (it.hasNext())
                ret.append(",");
        }
        ret.append("]");

        List<Playlist> pl = DB.get().getPlaylists(userId);
        ret.append(",\"playlists\":[");
        for (Iterator<Playlist> it = pl.iterator(); it.hasNext();) {
            Playlist p = it.next();
            ret.append(p.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, l1);
        return ret.toString();
    }

    @POST
    @Path("user/add")
    public String addUser(@FormParam("username") String username, @FormParam("password") String password,
            @FormParam("auth") int auth) throws SQLException, SecurityError {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);

        if (username == null || username.trim().length() == 0) {
            throw new IllegalArgumentException("Empty user name");
        }
        if (password == null || password.trim().length() < 4) {
            throw new IllegalArgumentException("Password too short");
        }

        StringBuilder ret = new StringBuilder();
        ret.append("{\"user\":");

        if (sid == null || sid.trim().length() == 0 && auth == 1) {
            // no user is present, accept the first admin creation
            // without any authentication !
            if (!DB.get().hasUsers()) {
                User u = new User();
                u.auth = auth;
                u.username = username;
                u.password = password.getBytes();
                u.hashPassword();

                u.id = DB.get().addUser(u);
                Logger.info("Added first user: " + username + " from " + request.getRemoteAddr());
                ret.append(u.toJSON());
                ret.append("}");
                return ret.toString();
            }
        }

        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        User prev = DB.get().getUser(username);
        if (prev != null) {
            throw new IllegalArgumentException("User " + username + " already exists");
        }
        if (auth != 1 && auth != 2) {
            throw new IllegalArgumentException("Invalid authorization level:" + auth);
        }

        User u = new User();
        u.auth = auth;
        u.username = username;
        u.password = password.getBytes();
        u.hashPassword();

        u.id = DB.get().addUser(u);

        ret.append(u.toJSON());
        ret.append("}");

        RuntimeStats.get().userCount.addAndGet(1);

        nocache();
        log(s, l);
        return ret.toString();
    }

    @POST
    @Path("user/password")
    public void setUserPassword(@FormParam("old_password") String oldPassword,
            @FormParam("new_password") String newPassword) throws SQLException, SecurityError {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        User user = DB.get().getUser(sess.getUserId());
        user.password = oldPassword.getBytes();
        if (!user.checkPassword()) {
            throw new SecurityError("Invalid old password");
        }

        user.password = newPassword.getBytes();
        user.hashPassword();
        DB.get().setPassword(user);

        nocache();
        log(sess, l);
    }

    @POST
    @Path("user/remove")
    public void removeUsers(@FormParam("user_ids[]") int[] user_ids) throws SQLException, SecurityError {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent, true);
        int uid = sess.getUserId();

        if (user_ids.length == 0) {
            throw new IllegalArgumentException("No user specified");
        }

        for (int user_id : user_ids) {
            if (uid == user_id) {
                throw new IllegalArgumentException("You cannot remove your own user.");
            }

            List<Session> sessions = Session.getSessions(user_id);
            for (Session s : sessions) {
                Session.remove(s);
            }
            DB.get().removeUser(user_id);
            RuntimeStats.get().userCount.addAndGet(-1);
        }

        nocache();
        log(sess, l);
    }

    @POST
    @Path("playlist/create")
    public String createPlaylist(@FormParam("name") String name) throws SQLException, SecurityError {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = s.getUserId();

        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Empty playlist name");
        }

        StringBuilder sb = new StringBuilder();
        Playlist pl = DB.get().addPlaylist(uid, name);
        RuntimeStats.get().playlistCount.addAndGet(1);
        sb.append("{\"playlist\":");
        sb.append(pl.toJSON());
        sb.append("}");

        nocache();
        log(s, l);
        return sb.toString();
    }

    @POST
    @Path("playlist/create-add")
    public String createAndAddToPlaylist(@FormParam("name") String name,
            @FormParam("clear") @DefaultValue("false") boolean clear, @FormParam("song_ids[]") int[] song_ids,
            @FormParam("album_ids[]") int[] album_ids)
            throws SQLException, SecurityError, javassist.NotFoundException, ForbiddenException {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = sess.getUserId();

        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Empty playlist name");
        }

        Playlist pl = DB.get().addPlaylist(uid, name);
        if (clear) {
            DB.get().clearPlaylist(pl.id, uid);
        }

        int count = 0;

        int[] arr = DB.get().getSongIds(album_ids);
        int[] s = new int[arr.length + song_ids.length];
        System.arraycopy(song_ids, 0, s, 0, song_ids.length);
        System.arraycopy(arr, 0, s, song_ids.length, arr.length);

        checkDuplicates(s);
        count = DB.get().addSongsToPlaylist(pl.id, s, uid);

        pl = DB.get().getPlaylist(pl.id);
        RuntimeStats.get().playlistCount.set(DB.get().getPlaylistCount());

        StringBuilder sb = new StringBuilder();
        sb.append("{ \"added\":" + count + ",");
        sb.append("\"playlist\":");
        sb.append(pl.toJSON());
        sb.append('}');

        nocache();
        log(sess, l);
        return sb.toString();
    }

    @POST
    @Path("playlist/random")
    public String randomPlaylist(@FormParam("name") String name, @FormParam("number") int number)
            throws SQLException, SecurityError, javassist.NotFoundException, ForbiddenException {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = sess.getUserId();

        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Empty playlist name");
        }

        if (number < 1)
            throw new IllegalArgumentException("Cannot create random playlist with 0 songs");
        if (number > 50)
            throw new IllegalArgumentException("Cannot create random playlist that big");

        Playlist pl = DB.get().addPlaylist(uid, name);
        if (pl.songs > 0) {
            DB.get().clearPlaylist(pl.id, uid);
        }

        List<Song> songs = DB.get().getRandomSongs(number);
        if (songs.size() == 0) {
            throw new IllegalStateException("There are currently no songs in the library");
        }

        int[] ids = new int[songs.size()];
        for (int i = 0; i < songs.size(); i++) {
            ids[i] = songs.get(i).id;
        }
        DB.get().addSongsToPlaylist(pl.id, ids, uid);
        pl = DB.get().getPlaylist(pl.id);
        RuntimeStats.get().playlistCount.set(DB.get().getPlaylistCount());

        StringBuilder sb = new StringBuilder();
        sb.append("{ \"added\":" + songs.size() + ",");
        sb.append("\"first_song\":" + ids[0] + ",");
        sb.append("\"playlist\":");
        sb.append(pl.toJSON());
        sb.append('}');

        nocache();
        log(sess, t1);
        return sb.toString();
    }

    @POST
    @Path("playlist/{playlist_id}/add")
    public String addSongsToPlaylist(@PathParam("playlist_id") int playlist_id,
            @FormParam("clear") @DefaultValue("false") boolean clear, @FormParam("song_ids[]") int[] song_ids,
            @FormParam("album_ids[]") int[] album_ids)
            throws SQLException, SecurityError, ForbiddenException, NotFoundException {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = sess.getUserId();
        int count = 0;

        if (clear) {
            DB.get().clearPlaylist(playlist_id, uid);
        }

        int[] arr = DB.get().getSongIds(album_ids);
        int[] s = new int[arr.length + song_ids.length];
        System.arraycopy(song_ids, 0, s, 0, song_ids.length);
        System.arraycopy(arr, 0, s, song_ids.length, arr.length);

        if (s.length == 0) {
            throw new IllegalArgumentException("No song or album provided");
        }

        checkDuplicates(s);

        count = DB.get().addSongsToPlaylist(playlist_id, s, uid);
        Playlist pl = DB.get().getPlaylist(playlist_id);

        StringBuilder sb = new StringBuilder();
        sb.append("{ \"added\":" + count + ",");
        sb.append("\"playlist\":");
        sb.append(pl.toJSON());
        sb.append('}');

        nocache();
        log(sess, t1);
        return sb.toString();
    }

    @POST
    @Path("playlist/{playlist_id}/remove")
    public void removeFromPlaylist(@PathParam("playlist_id") int playlist_id,
            @FormParam("song_ids[]") int[] song_ids) throws SQLException, SecurityError, ForbiddenException {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = s.getUserId();

        if (song_ids == null || song_ids.length == 0)
            throw new IllegalArgumentException("No song ids provided");

        DB.get().removeSongsFromPlaylist(playlist_id, song_ids, uid);

        nocache();
        log(s, l);
    }

    @GET
    @Path("playlist/{playlist_id}/songs")
    public String getPlaylistSongs(@PathParam("playlist_id") int playlist_id) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        Playlist pl = DB.get().getPlaylist(playlist_id);
        List<Song> songs = DB.get().getPlaylistSongs(playlist_id);

        if (pl == null) {
            throw new NotFoundException("No such playlist: " + playlist_id);
        }

        StringBuilder ret = new StringBuilder();
        ret.append("{\"name\":" + JSONObject.quote(pl.name) + ",");
        ret.append("\"playlist\":[");
        for (Iterator<Song> it = songs.iterator(); it.hasNext();) {
            Song s = it.next();
            ret.append(s.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("playlist/{playlist_id}/song/{song_pos}")
    public String getPlaylistSong(@PathParam("playlist_id") int playlist_id, @PathParam("song_pos") int song_pos)
            throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        StringBuilder ret = new StringBuilder();
        Song s = DB.get().getPlaylistSong(playlist_id, song_pos);
        if (s == null) {
            throw new NotFoundException("Could not find song " + song_pos + " in playlist " + playlist_id);
        }
        ret.append("{\"song\":");
        ret.append(s.toJSON());
        ret.append("}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @POST
    @Path("playlists/remove")
    public void deletePlaylists(@FormParam("playlist_ids[]") int[] playlist_ids)
            throws SQLException, SecurityError, NotFoundException, ForbiddenException {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);
        int uid = sess.getUserId();

        if (playlist_ids == null || playlist_ids.length == 0)
            throw new IllegalArgumentException("No playlist ids provided");

        int ret = DB.get().removePlaylists(playlist_ids, uid);
        RuntimeStats.get().playlistCount.addAndGet(-ret);

        nocache();
        log(sess, l);
    }

    @GET
    @Path("playlists")
    public String getPlaylists() throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent);

        List<Playlist> pl = DB.get().getPlaylists(s.getUserId());
        StringBuilder ret = new StringBuilder();
        ret.append("{\"playlists\":[");
        for (Iterator<Playlist> it = pl.iterator(); it.hasNext();) {
            Playlist p = it.next();
            ret.append(p.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(s, t1);
        return ret.toString();
    }

    @GET
    @Path("playlists/{user_id}")
    public String getPlaylists(@PathParam("user_id") int userId) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        List<Playlist> pl = DB.get().getPlaylists(userId);
        StringBuilder ret = new StringBuilder();
        ret.append("{\"playlists\":[");
        for (Iterator<Playlist> it = pl.iterator(); it.hasNext();) {
            Playlist p = it.next();
            ret.append(p.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("artists")
    public String getArtists() throws SQLException, SecurityError {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent);

        List<Artist> artists = DB.get().getArtists();
        Map<Integer, Map<Integer, String>> artworks = DB.get().getAlbumArtworks();

        StringBuilder ret = new StringBuilder();
        ret.append("{\"artists\":[");
        for (Iterator<Artist> it = artists.iterator(); it.hasNext();) {
            Artist ar = it.next();

            ret.append("{\"artist\":");
            ret.append(ar.toJSON());
            ret.append(",\"artworks\":[");
            Map<Integer, String> al = artworks.get(ar.id);
            if (al != null) {

                Iterator<Entry<Integer, String>> it2 = al.entrySet().iterator();
                while (it2.hasNext()) {
                    Entry<Integer, String> entry = it2.next();
                    int album_id = entry.getKey();
                    String artwork_id = entry.getValue();

                    ret.append("{\"album\":" + album_id + ",");
                    ret.append("\"id\":" + JSONObject.quote(artwork_id));
                    ret.append("}");
                    if (it2.hasNext()) {
                        ret.append(',');
                    }
                }
            }
            ret.append("]}");
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(s, l);
        return ret.toString();
    }

    @GET
    @Path("albums/{artist_id}")
    public String getAlbums(@PathParam("artist_id") int artist_id) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        Artist artist = DB.get().getArtist(artist_id);
        if (artist == null) {
            throw new NotFoundException("Cannot find artist " + artist_id);
        }
        List<Album> albums = DB.get().getAlbums(artist_id);

        StringBuilder ret = new StringBuilder();
        ret.append("{\"artist\":");
        ret.append(artist.toJSON());
        ret.append(",\"albums\":[");
        for (Iterator<Album> it = albums.iterator(); it.hasNext();) {
            Album al = it.next();
            ret.append(al.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("recent/{number}")
    public String getRecent(@PathParam("number") int number) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        List<Album> albums = DB.get().getLatestAlbums(number);
        List<Artist> artists = DB.get().getLatestArtists(number);

        StringBuilder ret = new StringBuilder();
        ret.append("{\"albums\":[");
        for (Iterator<Album> it = albums.iterator(); it.hasNext();) {
            Album al = it.next();
            ret.append(al.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("],\"artists\":[");
        for (Iterator<Artist> it = artists.iterator(); it.hasNext();) {
            Artist al = it.next();
            ret.append(al.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }

        ret.append("]}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("songs/{album_id}")
    public String getSongs(@PathParam("album_id") int album_id) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        List<Song> songs = DB.get().getSongs(album_id);
        Album album = DB.get().getAlbum(album_id);
        if (album == null) {
            throw new NotFoundException("Cannot find album " + album_id);
        }
        Artist artist = DB.get().getArtist(album.artist_id);

        StringBuilder ret = new StringBuilder();
        ret.append("{\"artist\":" + artist.toJSON() + ",");
        ret.append("\"album\":" + album.toJSON() + ",");
        ret.append("\"songs\":[");
        for (Iterator<Song> it = songs.iterator(); it.hasNext();) {
            Song s = it.next();
            ret.append(s.toJSON());
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("song/{song_id}")
    public String getSong(@PathParam("song_id") final int song_id) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        Song song = DB.get().getSong(song_id);
        if (song == null) {
            throw new NotFoundException("Cannot find song " + song_id);
        }
        Album album = DB.get().getAlbum(song.album_id);
        Artist artist = DB.get().getArtist(album.artist_id);

        StringBuilder ret = new StringBuilder();
        ret.append("{\"artist\":" + artist.toJSON() + ",");
        ret.append("\"album\":" + album.toJSON() + ",");
        ret.append("\"song\":" + song.toJSON() + "}");

        nocache();
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("search/{query}")
    public String search(@PathParam("query") final String query) throws SQLException, SecurityError {
        long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent);

        StringBuilder ret = new StringBuilder();

        List<Artist> artists = DB.get().searchArtist(query, 20);
        ret.append("{\"artists\":[");
        for (Iterator<Artist> it = artists.iterator(); it.hasNext();) {
            Artist ar = it.next();
            ret.append(ar.toJSON());
            if (it.hasNext())
                ret.append(',');
        }

        List<Album> albums = DB.get().searchAlbum(query, 20);
        ret.append("],\"albums\":[");
        for (Iterator<Album> it = albums.iterator(); it.hasNext();) {
            Album a = it.next();
            ret.append(a.toJSON());
            if (it.hasNext())
                ret.append(',');
        }

        List<Song> songs = DB.get().searchSong(query, 20);
        ret.append("],\"songs\":[");
        for (Iterator<Song> it = songs.iterator(); it.hasNext();) {
            Song s = it.next();
            ret.append(s.toJSON());
            if (it.hasNext())
                ret.append(',');
        }

        ret.append("]}");

        this.response.setHeader("Cache-Control", "max-age=60, must-revalidate");
        log(sess, t1);
        return ret.toString();
    }

    @GET
    @Path("song/{song_id}/stream")
    public Response getSong(@PathParam("song_id") final int song_id, @HeaderParam("range") final String range)
            throws SQLException, SecurityError {
        final long t1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        final Session s = Session.check(sid, request.getRemoteAddr(), userAgent);

        s.setLastPlayedSong(DB.get().getSong(song_id));

        final String filePath = DB.get().getSongFilePath(song_id);
        final File f = new File(filePath);

        // check the value for the 'range' http header, which
        // indicates when the client is seeking in the middle of a song
        int _startRange = 0;
        if (range != null) {
            Pattern pat = Pattern.compile("bytes=([0-9]+)-([0-9]*)");
            Matcher mat = pat.matcher(range);
            if (mat.matches()) {
                if (mat.group(1) != null) {
                    _startRange = Integer.parseInt(mat.group(1));
                }
            }
        }
        final long startRange = _startRange;
        final long endRange = f.length() - 1;
        final String contentRange = "bytes " + startRange + "-" + endRange + "/" + f.length();

        // this StreamingOutput object gives us a way to write the response to a
        // Stream,
        // which allows reading the file chunk by chunk to avoid having it all
        // in memory
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                int totalBytes = 0;
                InputStream in = new FileInputStream(f);
                byte[] bytes = new byte[8192];
                int bytesRead;

                try {
                    in.skip(startRange);
                    while ((bytesRead = in.read(bytes)) != -1) {
                        totalBytes += bytesRead;
                        out.write(bytes, 0, bytesRead);
                    }
                } catch (Throwable t) {
                    return;
                } finally {
                    RuntimeStats.get().downloaded.addAndGet(totalBytes);
                    try {
                        DB.get().updateDownloadedBytes(s.getUserId(), totalBytes);
                    } catch (SQLException e) {
                        Logger.error("Failed to update user stats", e);
                    }
                    in.close();
                }
            }
        };

        String contentType = "*/*";
        if (filePath.endsWith("mp3")) {
            contentType = "audio/mpeg";
        } else if (filePath.endsWith("mp4")) {
            contentType = "audio/aac";
        } else if (filePath.endsWith("aac")) {
            contentType = "audio/aac";
        } else if (filePath.endsWith("m4a")) {
            contentType = "audio/aac";
        } else if (filePath.endsWith("ogg")) {
            contentType = "audio/ogg";
        } else if (filePath.endsWith("wav")) {
            contentType = "audio/wav";
        }

        int status = 200;
        if (startRange > 0) {
            // seeking: HTTP 206 partial content
            status = 206;
        }

        log(s, t1);

        return Response.status(status) //
                .type(contentType) //
                .header("Content-Length", f.length() - startRange) //
                .header("Accept-Ranges", "bytes") //
                .header("Content-Range", contentRange) //
                .header("Cache-Control", "max-age=86400, must-revalidate") //
                .entity(stream) //
                .build();
    }

    @GET
    @Path("art/{album_id}")
    public Response getAlbumArtwork(@PathParam("album_id") int album_id) throws SQLException, SecurityError {

        final String art = DB.get().getAlbumArtwork(album_id);
        if (art == null || art.trim().length() == 0) {
            throw new NotFoundException("Not artwork for album " + album_id);
        }

        final File f = new File(art);

        if (!f.exists()) {
            throw new NotFoundException("Not artwork for album " + album_id);
        }

        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                InputStream in = new FileInputStream(f);
                byte[] bytes = new byte[8192];
                int bytesRead;
                int totalBytes = 0;

                try {
                    while ((bytesRead = in.read(bytes)) != -1) {
                        totalBytes += bytesRead;
                        out.write(bytes, 0, bytesRead);
                    }
                } catch (Throwable t) {
                    return;
                } finally {
                    RuntimeStats.get().downloaded.addAndGet(totalBytes);
                    in.close();
                }
            }
        };

        Logger.debug("GET art/" + album_id + " " + request.getRemoteAddr());
        return Response.status(200) //
                .type("image/jpeg") //
                .header("Content-Length", f.length()) //
                .header("Accept-Ranges", "bytes") //
                .header("Cache-Control", "max-age=86400, public") //
                .entity(stream) //
                .build();
    }

    @GET
    @Path("folders")
    public String getMusicFolders() throws SecurityError, SQLException {
        long l = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent, true);
        StringBuilder ret = new StringBuilder();

        ret.append("{\"folders\":[");
        for (Iterator<String> it = Config.getMusicPath().iterator(); it.hasNext();) {
            String path = it.next();
            ret.append(JSONObject.quote(path));
            if (it.hasNext()) {
                ret.append(',');
            }
        }
        ret.append("]}");

        nocache();
        log(sess, l);
        return ret.toString();
    }

    @GET
    @Path("folders/listing")
    public String getFolderListing(@QueryParam("directory") String directory) throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent, true);
        StringBuilder ret = new StringBuilder();

        File dir = null;
        File[] l = null;
        String dirName = null;

        if (directory == null || directory.trim().length() == 0) {
            dir = new File(System.getProperty("user.home"));
            l = dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.isDirectory();
                }
            });
            dirName = dir.getAbsolutePath();
        } else if (directory.equals("$ROOT")) {
            dirName = "";
            l = File.listRoots();
        } else {
            dir = new File(directory);

            if (!dir.exists()) {
                throw new NotFoundException("Unknown directory: " + directory);
            } else if (!dir.isDirectory()) {
                throw new IllegalArgumentException("Not a directory: " + directory);
            }

            l = dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.isDirectory();
                }
            });
            dirName = dir.getAbsolutePath();
        }

        ret.append("{\"directory\":" + JSONObject.quote(dirName));
        ret.append(",\"separator\":" + JSONObject.quote(File.separator));
        if (dir != null && dir.getParent() != null) {
            ret.append(",\"parent\":" + JSONObject.quote(dir.getParent()));
        } else {
            ret.append(",\"parent\":" + JSONObject.quote("$ROOT"));
        }
        ret.append(",\"listing\":[");
        if (l != null) {
            Arrays.sort(l);
            boolean virgule = false;
            for (int i = 0; i < l.length; i++) {
                if (l[i].exists()) {
                    if (virgule) {
                        ret.append(',');
                    }
                    virgule = (i + 1 < l.length);
                    ret.append(JSONObject.quote(l[i].getAbsolutePath()));
                }
            }
        }
        ret.append("]}");

        nocache();
        log(sess, l1);
        return ret.toString();
    }

    @POST
    @Path("folders/add")
    public void addMusicFolder(@FormParam("directory") String directory) throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        File dir = new File(directory);
        if (!dir.exists()) {
            throw new NotFoundException("Unknown directory: " + directory);
        } else if (!dir.isDirectory()) {
            throw new IllegalArgumentException("Not a directory: " + directory);
        }

        Config.getMusicPath().add(directory);
        DB.get().addFolder(directory);
        Library.interrupt();

        nocache();
        log(sess, l1);
    }

    @POST
    @Path("folders/remove")
    public void removeMusicFolders(@FormParam("directory[]") String[] directory)
            throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session sess = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        if (directory != null) {
            List<String> music = Config.getMusicPath();
            for (String dir : directory) {
                if (!music.contains(dir)) {
                    throw new IllegalArgumentException("Directory '" + dir + "' is not a music folder");
                }
            }

            for (String dir : directory) {
                File toDelete = new File(dir);
                for (Iterator<String> it = music.iterator(); it.hasNext();) {
                    String path = it.next();
                    File f = new File(path);
                    if (f.equals(toDelete)) {
                        it.remove();
                        DB.get().removeFolder(path);
                    }
                }
            }
        }
        Library.interrupt();

        nocache();
        log(sess, l1);
    }

    @GET
    @Path("indexer/status")
    public String getIndexerStatus() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        String ret = Library.getIndexerStatusAsJSON();

        log(s, l1);
        return ret;
    }

    @POST
    @Path("indexer/rescan")
    public void rescan() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        Library.interrupt();

        log(s, l1);
    }

    @POST
    @Path("edit/artist")
    public void editArtist(@FormParam("artist_ids[]") int[] artist_ids,
            @FormParam("artist_name") String artist_name) throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        List<String> files = DB.get().getArtistSongPaths(artist_ids);
        if (files.isEmpty()) {
            throw new NotFoundException("No artist found");
        }

        Library.editArtist(files, artist_name);
        DB.get().editArtist(artist_ids, artist_name);
        RuntimeStats.get().updateFromDB();

        log(s, l1);
    }

    @POST
    @Path("edit/album")
    public void editAlbum(@FormParam("album_ids[]") int[] album_ids, @FormParam("album_name") String album_name,
            @FormParam("artist_name") String artist_name, @FormParam("date") int date,
            @FormParam("genre") String genre) throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        List<String> files = DB.get().getAlbumSongPaths(album_ids);
        if (files.isEmpty()) {
            throw new NotFoundException("No album found ");
        }

        Library.editAlbum(files, album_name, artist_name, date, genre);
        DB.get().editAlbum(album_ids, album_name, artist_name, date, genre);
        RuntimeStats.get().updateFromDB();

        log(s, l1);
    }

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Path("edit/artwork/{album_id}")
    public void editArtwork(@PathParam("album_id") int album_id, MultipartFormDataInput multipart)
            throws SQLException, SecurityError {

        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        String path = null;
        try {
            InputPart part = multipart.getFormDataMap().get("file").get(0);
            InputStream is = part.getBody(new GenericType<InputStream>() {
            });
            File file = File.createTempFile("image.", ".tmp");
            FileUtils.copyInputStreamToFile(is, file);
            path = Library.resizeArtwork(file.getAbsolutePath());
            file.delete();
        } catch (IOException e) {
            Logger.error("Failed to create image file", e);
            throw new IllegalArgumentException("Failed to create new artwork image");
        }
        int[] ids = new int[] { album_id };
        List<String> files = DB.get().getAlbumSongPaths(ids);
        Library.editArtwork(files, path);
        DB.get().editAlbumArtwork(ids, path);

        log(s, l1);
    }

    @POST
    @Path("edit/song")
    public void editSong(@FormParam("song_ids[]") int[] song_ids, @FormParam("song_title") String song_title,
            @FormParam("position") int position, @FormParam("disc_no") int disc_no,
            @FormParam("album_name") String album_name, @FormParam("artist_name") String artist_name)
            throws SecurityError, SQLException {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        List<String> files = DB.get().getSongPaths(song_ids);
        if (files.isEmpty()) {
            throw new NotFoundException("No song found ");
        }

        List<Song> songs = DB.get().getSongs(song_ids);
        int album_id = songs.get(0).album_id;
        for (Song song : songs) {
            if (album_id != song.album_id) {
                throw new IllegalArgumentException("All edited songs should belong to the same album.");
            }
        }

        Library.editSong(files, song_title, position, disc_no, album_name, artist_name, 0, null);
        DB.get().editSong(song_ids, song_title, position, disc_no, album_name, artist_name);
        RuntimeStats.get().updateFromDB();

        log(s, l1);
    }

    @GET
    @Path("logs")
    public Response getLogs() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        final File log = new File(Config.getLogFilePath());
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {

                int totalBytes = 0;
                InputStream in = new FileInputStream(log);
                byte[] bytes = new byte[8192];
                int bytesRead;

                try {
                    while ((bytesRead = in.read(bytes)) != -1) {
                        totalBytes += bytesRead;
                        out.write(bytes, 0, bytesRead);
                    }
                } catch (Throwable e) {
                    Logger.warn("GET log interrupted after " + totalBytes + "B", e);
                } finally {
                    in.close();
                }
            }
        };

        nocache();
        log(s, l1);

        return Response.status(200) //
                .type("text/plain") //
                .header("Content-Length", log.length()) //
                .entity(stream) //
                .build();
    }

    @POST
    @Path("shutdown")
    public void shutdown() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, true);

        Bootstrap.shutdown();
        log(s, l1);
        System.exit(0);
    }

    @GET
    @Path("stats")
    public String getStats() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, false);
        StringBuilder sb = new StringBuilder();

        sb.append("{\"stats\":" + RuntimeStats.get().toJSON() + "}");

        nocache();
        log(s, l1);
        return sb.toString();
    }

    @GET
    @Path("info")
    public String getInfo() throws SecurityError {
        long l1 = System.nanoTime();
        String sid = (sessionIdHeader == null ? sessionIdGet : sessionIdHeader);
        Session s = Session.check(sid, request.getRemoteAddr(), userAgent, false);

        StringBuilder sb = new StringBuilder();
        sb.append('{');
        sb.append("\"version\":" + JSONObject.quote(Config.getVersion()) + ",");
        sb.append("\"build\":" + JSONObject.quote(Config.getBuildInfo()) + ",");
        sb.append("\"server\":" + JSONObject.quote(Config.getServerInfo()) + ",");
        sb.append("\"os\":" + JSONObject.quote(Config.getOsInfo()) + ",");
        sb.append("\"java\":" + JSONObject.quote(Config.getJavaInfo()));
        sb.append('}');

        nocache();
        log(s, l1);

        return sb.toString();
    }

    private void checkDuplicates(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
                if (j != i && arr[i] == arr[j]) {
                    throw new IllegalStateException("Duplicate id: " + arr[i]);
                }
            }

        }
    }
}