com.limegroup.gnutella.gui.DaapManager.java Source code

Java tutorial

Introduction

Here is the source code for com.limegroup.gnutella.gui.DaapManager.java

Source

package com.limegroup.gnutella.gui;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.BindException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManagerEvent;
import com.limegroup.gnutella.IncompleteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.settings.DaapSettings;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLNames;
import com.limegroup.gnutella.xml.LimeXMLReplyCollection;
import com.limegroup.gnutella.xml.SchemaReplyCollectionMapper;

import de.kapsi.net.daap.DaapAuthenticator;
import de.kapsi.net.daap.DaapConfig;
import de.kapsi.net.daap.DaapFilter;
import de.kapsi.net.daap.DaapServer;
import de.kapsi.net.daap.DaapServerFactory;
import de.kapsi.net.daap.DaapStreamSource;
import de.kapsi.net.daap.DaapThreadFactory;
import de.kapsi.net.daap.DaapUtil;
import de.kapsi.net.daap.Database;
import de.kapsi.net.daap.Library;
import de.kapsi.net.daap.Playlist;
import de.kapsi.net.daap.Song;
import de.kapsi.net.daap.Transaction;
import de.kapsi.net.daap.TransactionListener;

/**
 * This class handles the mDNS registration and acts as an
 * interface between LimeWire and DAAP.
 */
public final class DaapManager implements FinalizeListener {

    private static final Log LOG = LogFactory.getLog(DaapManager.class);
    private static final DaapManager INSTANCE = new DaapManager();

    public static DaapManager instance() {
        return INSTANCE;
    }

    private SongURNMap map;

    private Library library;
    private Database database;
    private Playlist whatsNew;
    private Playlist creativecommons;
    private Playlist videos;

    private DaapServer server;
    private RendezvousService rendezvous;

    private boolean enabled = false;
    private int maxPlaylistSize;

    private DaapManager() {
        GUIMediator.addFinalizeListener(this);
    }

    /**
     * Initializes the Library
     */
    public synchronized void init() {

        if (isServerRunning()) {
            setEnabled(enabled);
        }
    }

    /**
     * Starts the DAAP Server
     */
    public synchronized void start() throws IOException {

        if (!isServerRunning()) {

            try {

                InetAddress addr = InetAddress.getLocalHost();

                if (addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) {
                    addr = null;
                    Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
                    if (interfaces != null) {
                        while (addr == null && interfaces.hasMoreElements()) {
                            NetworkInterface nif = (NetworkInterface) interfaces.nextElement();
                            Enumeration addresses = nif.getInetAddresses();
                            while (addresses.hasMoreElements()) {
                                InetAddress address = (InetAddress) addresses.nextElement();
                                if (!address.isLoopbackAddress() && address instanceof Inet4Address) {
                                    addr = address;
                                    break;
                                }
                            }
                        }
                    }
                }

                if (addr == null) {
                    stop();
                    // No valid IP address -- just ignore, since
                    // it's probably the user isn't connected to the
                    // internet.  Next time they start, it might work.
                    return;
                }

                rendezvous = new RendezvousService(addr);

                map = new SongURNMap();

                maxPlaylistSize = DaapSettings.DAAP_MAX_LIBRARY_SIZE.getValue();

                String name = DaapSettings.DAAP_LIBRARY_NAME.getValue();
                int revisions = DaapSettings.DAAP_LIBRARY_REVISIONS.getValue();
                boolean useLibraryGC = DaapSettings.DAAP_LIBRARY_GC.getValue();
                library = new Library(name, revisions, useLibraryGC);

                database = new Database(name);
                whatsNew = new Playlist(GUIMediator.getStringResource("SEARCH_TYPE_WHATSNEW"));
                creativecommons = new Playlist(GUIMediator.getStringResource("LICENSE_CC"));
                videos = new Playlist(GUIMediator.getStringResource("MEDIA_VIDEO"));

                Transaction txn = library.open(false);
                library.add(txn, database);
                database.add(txn, creativecommons);
                database.add(txn, whatsNew);
                database.add(txn, videos);
                creativecommons.setSmartPlaylist(txn, true);
                whatsNew.setSmartPlaylist(txn, true);
                videos.setSmartPlaylist(txn, true);
                txn.commit();

                LimeConfig config = new LimeConfig(addr);

                final boolean NIO = DaapSettings.DAAP_USE_NIO.getValue();

                server = DaapServerFactory.createServer(library, config, NIO);

                server.setAuthenticator(new LimeAuthenticator());
                server.setStreamSource(new LimeStreamSource());
                server.setFilter(new LimeFilter());

                if (!NIO) {
                    server.setThreadFactory(new LimeThreadFactory());
                }

                final int maxAttempts = 10;

                for (int i = 0; i < maxAttempts; i++) {
                    try {
                        server.bind();
                        break;
                    } catch (BindException bindErr) {
                        if (i < (maxAttempts - 1)) {
                            // try next port...
                            config.nextPort();
                        } else {
                            throw bindErr;
                        }
                    }
                }

                Thread serverThread = new ManagedThread(server, "DaapServerThread") {
                    protected void managedRun() {
                        try {
                            super.managedRun();
                        } catch (Throwable t) {
                            DaapManager.this.stop();
                            if (!handleError(t)) {
                                GUIMediator.showError("ERROR_DAAP_RUN_ERROR");
                                DaapSettings.DAAP_ENABLED.setValue(false);
                                if (t instanceof RuntimeException)
                                    throw (RuntimeException) t;
                                throw new RuntimeException(t);
                            }
                        }
                    }
                };

                serverThread.setDaemon(true);
                serverThread.start();

                rendezvous.registerService();

            } catch (IOException err) {
                stop();
                throw err;
            }
        }
    }

    /**
     * Stops the DAAP Server and releases all resources
     */
    public synchronized void stop() {

        if (rendezvous != null)
            rendezvous.close();

        if (server != null) {
            server.stop();
            server = null;
        }

        if (map != null)
            map.clear();

        rendezvous = null;

        map = null;
        library = null;
        whatsNew = null;
        creativecommons = null;
        database = null;
    }

    /**
     * Restarts the DAAP server and re-registers it via mDNS.
     * This is equivalent to:<p>
     *
     * <code>
     * stop();
     * start();
     * init();
     * </code>
     */
    public synchronized void restart() throws IOException {
        if (isServerRunning())
            stop();

        start();
        init();
    }

    /**
     * Shutdown the DAAP service properly. In this case
     * is the main focus on mDNS (Rendezvous) as in
     * some rare cases iTunes doesn't recognize that
     * LimeWire/DAAP is no longer online.
     */
    public void doFinalize() {
        stop();
    }

    /**
     * Updates the multicast-DNS servive info
     */
    public synchronized void updateService() throws IOException {

        if (isServerRunning()) {
            rendezvous.updateService();

            Transaction txn = library.open(false);
            String name = DaapSettings.DAAP_LIBRARY_NAME.getValue();
            library.setName(txn, name);
            database.setName(txn, name);
            txn.commit();
            server.update();
        }
    }

    /**
     * Disconnects all clients
     */
    public synchronized void disconnectAll() {
        if (isServerRunning()) {
            server.disconnectAll();
        }
    }

    /**
     * Returns <tt>true</tt> if server is running
     */
    public synchronized boolean isServerRunning() {
        if (server != null) {
            return server.isRunning();
        }
        return false;
    }

    /**
     * Attempts to handle an exception.
     * Returns true if we could handle it correctly.
     */
    private boolean handleError(Throwable t) {
        if (t == null)
            return false;

        String msg = t.getMessage();
        if (msg == null || msg.indexOf("Unable to establish loopback connection") == -1)
            return handleError(t.getCause());

        // Problem with XP SP2. -- Loopback connections are disallowed.
        // Why?  Who knows.  This patch fixes it:
        // http://support.microsoft.com/default.aspx?kbid=884020
        if (CommonUtils.isWindowsXP()) {
            int answer = GUIMediator.showYesNoCancelMessage("ERROR_DAAP_LOOPBACK_FAILED");
            switch (answer) {
            case GUIMediator.YES_OPTION:
                GUIMediator.openURL("http://support.microsoft.com/default.aspx?kbid=884020");
                break;
            case GUIMediator.NO_OPTION:
                DaapSettings.DAAP_ENABLED.setValue(false);
                break;
            }
        } else {
            // Also a problem on non XP systems with firewalls.
            int answer = GUIMediator.showYesNoMessage("ERROR_DAAP_LOOPBACK_FAILED_NONXP");
            if (answer == GUIMediator.NO_OPTION)
                DaapSettings.DAAP_ENABLED.setValue(false);
        }

        return true;
    }

    /**
     * Returns true if the extension of name is a supported file type.
     */
    private static boolean isSupportedAudioFormat(String name) {
        return isSupportedFormat(DaapSettings.DAAP_SUPPORTED_AUDIO_FILE_TYPES.getValue(), name);
    }

    private static boolean isSupportedVideoFormat(String name) {
        return isSupportedFormat(DaapSettings.DAAP_SUPPORTED_VIDEO_FILE_TYPES.getValue(), name);
    }

    private static boolean isSupportedFormat(String[] types, String name) {
        for (int i = 0; i < types.length; i++) {
            if (name.endsWith(types[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Handles a change event.
     */
    private void handleChangeEvent(FileManagerEvent evt) {
        FileDesc oldDesc = evt.getFileDescs()[0];
        Song song = map.remove(oldDesc.getSHA1Urn());

        if (song != null) {
            FileDesc newDesc = evt.getFileDescs()[1];
            map.put(song, newDesc.getSHA1Urn());

            // Any changes in the meta data?
            if (updateSongAudioMeta(song, newDesc) || updateSongVideoMeta(song, newDesc)) {
                Transaction txn = library.open(true);
                txn.addTransactionListener(new ServerUpdater(server));
                database.update(txn, song);
            }
        }
    }

    /**
     * Handles an add event.
     */
    private void handleAddEvent(FileManagerEvent evt) {
        if (database.getMasterPlaylist().size() >= maxPlaylistSize)
            return;

        FileDesc file = evt.getFileDescs()[0];
        if (!(file instanceof IncompleteFileDesc)) {
            String name = file.getFileName().toLowerCase(Locale.US);

            Song song = null;

            if (isSupportedAudioFormat(name)) {
                song = createSong(file, true);
            } else if (isSupportedVideoFormat(name)) {
                song = createSong(file, false);
            }

            if (song != null) {
                map.put(song, file.getSHA1Urn());

                Transaction txn = library.open(true);
                txn.addTransactionListener(new ServerUpdater(server));

                database.getMasterPlaylist().add(txn, song);
                whatsNew.add(txn, song);

                if (file.isLicensed()) {
                    creativecommons.add(txn, song);
                }

                if (isSupportedVideoFormat(name)) {
                    videos.add(txn, song);
                }
            }
        }
    }

    /**
     * Handles a rename event.
     */
    private void handleRenameEvent(FileManagerEvent evt) {
        FileDesc oldDesc = evt.getFileDescs()[0];
        Song song = map.remove(oldDesc.getSHA1Urn());

        if (song != null) {
            FileDesc newDesc = evt.getFileDescs()[1];
            map.put(song, newDesc.getSHA1Urn());
        }
    }

    /**
     * Handles a remove event.
     */
    private void handleRemoveEvent(FileManagerEvent evt) {
        FileDesc file = evt.getFileDescs()[0];
        Song song = map.remove(file.getSHA1Urn());

        if (song != null) {
            Transaction txn = library.open(true);
            txn.addTransactionListener(new ServerUpdater(server));
            database.remove(txn, song);
        }
    }

    /**
     * Called by VisualConnectionCallback
     */
    public synchronized void handleFileManagerEvent(FileManagerEvent evt) {
        if (!enabled || !isServerRunning())
            return;

        if (evt.isChangeEvent())
            handleChangeEvent(evt);
        else if (evt.isAddEvent())
            handleAddEvent(evt);
        else if (evt.isRenameEvent())
            handleRenameEvent(evt);
        else if (evt.isRemoveEvent())
            handleRemoveEvent(evt);
    }

    /**
     * Called by VisualConnectionCallback/MetaFileManager.
     */
    public void fileManagerLoading() {
        setEnabled(false);
    }

    /**
     * Called by VisualConnectionCallback/MetaFileManager.
     */
    public void fileManagerLoaded() {
        setEnabled(true);
    }

    public synchronized boolean isEnabled() {
        return enabled;
    }

    private synchronized void setEnabled(boolean enabled) {

        this.enabled = enabled;
        //System.out.println("setEnabled: " + enabled);

        if (!enabled || !isServerRunning())
            return;

        int size = database.getMasterPlaylist().size();
        Transaction txn = library.open(false);
        SongURNMap tmpMap = new SongURNMap();
        FileDesc[] files = RouterService.getFileManager().getAllSharedFileDescriptors();

        for (int i = 0; i < files.length; i++) {
            FileDesc file = files[i];
            if (file instanceof IncompleteFileDesc) {
                continue;
            }

            String name = file.getFileName().toLowerCase(Locale.US);
            boolean audio = isSupportedAudioFormat(name);

            if (!audio && !isSupportedVideoFormat(name)) {
                continue;
            }

            URN urn = file.getSHA1Urn();

            // 1)
            // _Remove_ URN from the current 'map'...
            Song song = map.remove(urn);

            // Check if URN is already in the tmpMap.
            // If so do nothing as we don't want add 
            // the same file multible times...
            if (tmpMap.contains(urn)) {
                continue;
            }

            // This URN was already mapped with a Song.
            // Save the Song (again) and update the meta
            // data if necessary
            if (song != null) {
                tmpMap.put(song, urn);

                // Any changes in the meta data?
                if ((audio && updateSongAudioMeta(song, file)) || updateSongVideoMeta(song, file)) {
                    database.update(txn, song);
                }

            } else if (size < maxPlaylistSize) {
                // URN was unknown and we must create a
                // new Song for this URN...
                song = createSong(file, audio);
                tmpMap.put(song, urn);
                database.getMasterPlaylist().add(txn, song);

                if (file.isLicensed()) {
                    creativecommons.add(txn, song);
                }

                if (isSupportedVideoFormat(name)) {
                    videos.add(txn, song);
                }

                size++;
            }
        }

        // See 1)
        // As all known URNs were removed from 'map' only
        // deleted FileDesc URNs can be leftover! We must 
        // remove the associated Songs from the Library now
        Iterator it = map.getSongIterator();
        while (it.hasNext()) {
            Song song = (Song) it.next();
            database.remove(txn, song);
        }

        map.clear();
        map = tmpMap; // tempMap is the new 'map'

        txn.addTransactionListener(new ServerUpdater(server));
        txn.commit();
    }

    /**
     * Create a Song and sets its meta data with
     * the data which is retrieved from the FileDesc
     */
    private Song createSong(FileDesc desc, boolean audio) {

        Song song = new Song(desc.getFileName());
        song.setSize((int) desc.getFileSize());
        song.setDateAdded((int) (System.currentTimeMillis() / 1000));

        File file = desc.getFile();
        String ext = FileUtils.getFileExtension(file);

        if (ext != null) {

            // Note: This is required for formats other than MP3
            // For example AAC (.m4a) files won't play if no
            // format is set. As far as I can tell from the iTunes
            // 'Get Info' dialog are Songs assumed as MP3 until
            // a format is set explicit.

            song.setFormat(ext.toLowerCase(Locale.US));

            if (audio) {
                updateSongAudioMeta(song, desc);
            } else {
                updateSongVideoMeta(song, desc);
            }
        }

        return song;
    }

    private boolean updateSongVideoMeta(Song song, FileDesc desc) {
        SchemaReplyCollectionMapper map = SchemaReplyCollectionMapper.instance();
        LimeXMLReplyCollection collection = map.getReplyCollection(LimeXMLNames.VIDEO_SCHEMA);

        if (collection == null) {
            LOG.error("LimeXMLReplyCollection is null");
            return false;
        }

        LimeXMLDocument doc = collection.getDocForHash(desc.getSHA1Urn());

        if (doc == null) {
            return false;
        }

        boolean update = false;

        String title = doc.getValue(LimeXMLNames.VIDEO_TITLE);
        //String type = doc.getValue(LimeXMLNames.VIDEO_TYPE);
        String year = doc.getValue(LimeXMLNames.VIDEO_YEAR);
        String rating = doc.getValue(LimeXMLNames.VIDEO_RATING);
        String length = doc.getValue(LimeXMLNames.VIDEO_LENGTH);
        //String comments = doc.getValue(LimeXMLNames.VIDEO_COMMENTS);
        //String licensetype = doc.getValue(LimeXMLNames.VIDEO_LICENSETYPE);
        String license = doc.getValue(LimeXMLNames.VIDEO_LICENSE);
        //String height = doc.getValue(LimeXMLNames.VIDEO_HEIGHT);
        //String width = doc.getValue(LimeXMLNames.VIDEO_WIDTH);
        String bitrate = doc.getValue(LimeXMLNames.VIDEO_BITRATE);
        //String action = doc.getValue(LimeXMLNames.VIDEO_ACTION);
        String director = doc.getValue(LimeXMLNames.VIDEO_DIRECTOR);
        //String studio = doc.getValue(LimeXMLNames.VIDEO_STUDIO);
        //String language = doc.getValue(LimeXMLNames.VIDEO_LANGUAGE);
        //String stars = doc.getValue(LimeXMLNames.VIDEO_STARS);
        //String producer = doc.getValue(LimeXMLNames.VIDEO_PRODUCE);
        //String subtitles = doc.getValue(LimeXMLNames.VIDEO_SUBTITLES);

        if (title != null) {
            String currentTitle = song.getName();
            if (currentTitle == null || !title.equals(currentTitle)) {
                update = true;
                song.setName(title);
            }
        }

        int currentBitrate = song.getBitrate();
        if (bitrate != null) {
            try {
                int num = Integer.parseInt(bitrate);
                if (num > 0 && num != currentBitrate) {
                    update = true;
                    song.setBitrate(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentBitrate != 0) {
            update = true;
            song.setBitrate(0);
        }

        int currentLength = song.getTime();
        if (length != null) {
            try {
                // iTunes expects the song length in milliseconds
                int num = (int) Integer.parseInt(length) * 1000;
                if (num > 0 && num != currentLength) {
                    update = true;
                    song.setTime(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentLength != 0) {
            update = true;
            song.setTime(0);
        }

        int currentYear = song.getYear();
        if (year != null) {
            try {
                int num = Integer.parseInt(year);
                if (num > 0 && num != currentYear) {
                    update = true;
                    song.setYear(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentYear != 0) {
            update = true;
            song.setYear(0);
        }

        // Genre = License
        String currentGenre = song.getGenre();
        if (license != null) {
            if (currentGenre == null || !license.equals(currentGenre)) {
                update = true;
                song.setGenre(license);
            }
        } else if (currentGenre != null) {
            update = true;
            song.setGenre(null);
        }

        // Artist = Director
        String currentArtist = song.getArtist();
        if (director != null) {
            if (currentArtist == null || !director.equals(currentArtist)) {
                update = true;
                song.setArtist(director);
            }
        } else if (currentArtist != null) {
            update = true;
            song.setArtist(null);
        }

        // Rating = Album
        String currentAlbum = song.getAlbum();
        if (rating != null) {
            if (currentAlbum == null || !rating.equals(currentAlbum)) {
                update = true;
                song.setAlbum(rating);
            }
        } else if (currentAlbum != null) {
            update = true;
            song.setAlbum(null);
        }

        return update;
    }

    /**
     * Sets the audio meta data
     */
    private boolean updateSongAudioMeta(Song song, FileDesc desc) {

        SchemaReplyCollectionMapper map = SchemaReplyCollectionMapper.instance();
        LimeXMLReplyCollection collection = map.getReplyCollection(LimeXMLNames.AUDIO_SCHEMA);

        if (collection == null) {
            LOG.error("LimeXMLReplyCollection is null");
            return false;
        }

        LimeXMLDocument doc = collection.getDocForHash(desc.getSHA1Urn());

        if (doc == null)
            return false;

        boolean update = false;

        String title = doc.getValue(LimeXMLNames.AUDIO_TITLE);
        String track = doc.getValue(LimeXMLNames.AUDIO_TRACK);
        String artist = doc.getValue(LimeXMLNames.AUDIO_ARTIST);
        String album = doc.getValue(LimeXMLNames.AUDIO_ALBUM);
        String genre = doc.getValue(LimeXMLNames.AUDIO_GENRE);
        String bitrate = doc.getValue(LimeXMLNames.AUDIO_BITRATE);
        //String comments = doc.getValue(LimeXMLNames.AUDIO_COMMENTS);
        String time = doc.getValue(LimeXMLNames.AUDIO_SECONDS);
        String year = doc.getValue(LimeXMLNames.AUDIO_YEAR);

        if (title != null) {
            String currentTitle = song.getName();
            if (currentTitle == null || !title.equals(currentTitle)) {
                update = true;
                song.setName(title);
            }
        }

        int currentTrack = song.getTrackNumber();
        if (track != null) {
            try {
                int num = Integer.parseInt(track);
                if (num > 0 && num != currentTrack) {
                    update = true;
                    song.setTrackNumber(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentTrack != 0) {
            update = true;
            song.setTrackNumber(0);
        }

        String currentArtist = song.getArtist();
        if (artist != null) {
            if (currentArtist == null || !artist.equals(currentArtist)) {
                update = true;
                song.setArtist(artist);
            }
        } else if (currentArtist != null) {
            update = true;
            song.setArtist(null);
        }

        String currentAlbum = song.getAlbum();
        if (album != null) {
            if (currentAlbum == null || !album.equals(currentAlbum)) {
                update = true;
                song.setAlbum(album);
            }
        } else if (currentAlbum != null) {
            update = true;
            song.setAlbum(null);
        }

        String currentGenre = song.getGenre();
        if (genre != null) {
            if (currentGenre == null || !genre.equals(currentGenre)) {
                update = true;
                song.setGenre(genre);
            }
        } else if (currentGenre != null) {
            update = true;
            song.setGenre(null);
        }

        /*String currentComments = song.getComment();
        if (comments != null) {
        if (currentComments == null || !comments.equals(currentComments)) {
            update = true;
            song.setComment(comments);
        }
        } else if (currentComments != null) {
        update = true;
        song.setComment(null);
        }*/

        int currentBitrate = song.getBitrate();
        if (bitrate != null) {
            try {
                int num = Integer.parseInt(bitrate);
                if (num > 0 && num != currentBitrate) {
                    update = true;
                    song.setBitrate(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentBitrate != 0) {
            update = true;
            song.setBitrate(0);
        }

        int currentTime = song.getTime();
        if (time != null) {
            try {
                // iTunes expects the song length in milliseconds
                int num = (int) Integer.parseInt(time) * 1000;
                if (num > 0 && num != currentTime) {
                    update = true;
                    song.setTime(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentTime != 0) {
            update = true;
            song.setTime(0);
        }

        int currentYear = song.getYear();
        if (year != null) {
            try {
                int num = Integer.parseInt(year);
                if (num > 0 && num != currentYear) {
                    update = true;
                    song.setYear(num);
                }
            } catch (NumberFormatException err) {
            }
        } else if (currentYear != 0) {
            update = true;
            song.setYear(0);
        }

        // iTunes expects the date/time in seconds
        int mod = (int) (desc.lastModified() / 1000);
        if (song.getDateModified() != mod) {
            update = true;
            song.setDateModified(mod);
        }

        return update;
    }

    /**
     * This factory creates ManagedThreads for the DAAP server
     */
    private final class LimeThreadFactory implements DaapThreadFactory {

        public Thread createDaapThread(Runnable runner, String name) {
            Thread thread = new ManagedThread(runner, name);
            thread.setDaemon(true);
            return thread;
        }
    }

    /**
     * Handles the audio stream
     */
    private final class LimeStreamSource implements DaapStreamSource {

        public FileInputStream getSource(Song song) throws IOException {
            URN urn = map.get(song);

            if (urn != null) {
                FileDesc fileDesc = RouterService.getFileManager().getFileDescForUrn(urn);
                if (fileDesc != null)
                    return new FileInputStream(fileDesc.getFile());
            }

            return null;
        }
    }

    /**
     * Implements the DaapAuthenticator
     */
    private final class LimeAuthenticator implements DaapAuthenticator {

        public boolean requiresAuthentication() {
            return DaapSettings.DAAP_REQUIRES_PASSWORD.getValue();
        }

        /**
         * Returns true if username and password are correct.<p>
         * Note: iTunes does not support usernames (i.e. it's
         * don't care)!
         */
        public boolean authenticate(String username, String password) {
            return DaapSettings.DAAP_PASSWORD.equals(password);
        }
    }

    /**
     * The DAAP Library should be only accessable from the LAN
     * as we can not guarantee for the required bandwidth and it
     * could be used to bypass Gnutella etc. Note: iTunes can't
     * connect to DAAP Libraries outside of the LAN but certain
     * iTunes download tools can.
     */
    private final class LimeFilter implements DaapFilter {

        /**
         * Returns true if <tt>address</tt> is a private address
         */
        public boolean accept(InetAddress address) {

            byte[] addr = address.getAddress();
            try {
                // not private & not close, not allowed.
                if (!NetworkUtils.isVeryCloseIP(addr) && !NetworkUtils.isPrivateAddress(addr))
                    return false;
            } catch (IllegalArgumentException err) {
                LOG.error(err);
                return false;
            }

            // Is it a annoying fellow? >:-)
            return IPFilter.instance().allow(addr);
        }
    }

    /**
     * A LimeWire specific implementation of DaapConfig
     */
    private final class LimeConfig implements DaapConfig {

        private InetAddress addr;

        public LimeConfig(InetAddress addr) {
            this.addr = addr;

            // Reset PORT to default value to prevent increasing
            // it to infinity
            DaapSettings.DAAP_PORT.revertToDefault();
        }

        public String getServerName() {
            return CommonUtils.getHttpServer();
        }

        public void nextPort() {
            int port = DaapSettings.DAAP_PORT.getValue();
            DaapSettings.DAAP_PORT.setValue(port + 1);
        }

        public int getBacklog() {
            return 0;
        }

        public InetSocketAddress getInetSocketAddress() {
            int port = DaapSettings.DAAP_PORT.getValue();
            return new InetSocketAddress(addr, port);
        }

        public int getMaxConnections() {
            return DaapSettings.DAAP_MAX_CONNECTIONS.getValue();
        }
    }

    /**
     * Helps us to publicize and update the DAAP Service via
     * multicast-DNS (aka Rendezvous or Zeroconf)
     */
    private final class RendezvousService {

        private static final String VERSION = "Version";
        private static final String MACHINE_NAME = "Machine Name";
        private static final String PASSWORD = "Password";

        private final JmDNS zeroConf;
        private ServiceInfo service;

        public RendezvousService(InetAddress addr) throws IOException {
            zeroConf = new JmDNS(addr);
        }

        public boolean isRegistered() {
            return (service != null);
        }

        private ServiceInfo createServiceInfo() {

            String type = DaapSettings.DAAP_TYPE_NAME.getValue();
            String name = DaapSettings.DAAP_SERVICE_NAME.getValue();

            int port = DaapSettings.DAAP_PORT.getValue();
            int weight = DaapSettings.DAAP_WEIGHT.getValue();
            int priority = DaapSettings.DAAP_PRIORITY.getValue();

            boolean password = DaapSettings.DAAP_REQUIRES_PASSWORD.getValue();

            java.util.Hashtable props = new java.util.Hashtable();

            // Greys the share and the playlist names when iTunes's
            // protocol version is different from this version. It's
            // only a nice visual effect and has no impact to the
            // ability to connect this server! Disabled because 
            // iTunes 4.2 is still widespread...
            props.put(VERSION, Integer.toString(DaapUtil.VERSION_3));

            // This is the inital share name
            props.put(MACHINE_NAME, name);

            // shows the small lock if Service is protected
            // by a password!
            props.put(PASSWORD, Boolean.toString(password));

            String qualifiedName = null;

            // This isn't really required but as iTunes
            // does it in this way I'm doing it too...
            if (password) {
                qualifiedName = name + "_PW." + type;
            } else {
                qualifiedName = name + "." + type;
            }

            ServiceInfo serviceInfo = new ServiceInfo(type, qualifiedName, port, weight, priority, props);

            return serviceInfo;
        }

        public void registerService() throws IOException {

            if (isRegistered())
                throw new IOException();

            ServiceInfo serviceInfo = createServiceInfo();
            zeroConf.registerService(serviceInfo);
            this.service = serviceInfo;
        }

        public void unregisterService() {
            if (!isRegistered())
                return;

            zeroConf.unregisterService(service);
            service = null;
        }

        public void updateService() throws IOException {
            if (!isRegistered())
                throw new IOException();

            if (service.getPort() != DaapSettings.DAAP_PORT.getValue())
                unregisterService();

            ServiceInfo serviceInfo = createServiceInfo();
            zeroConf.registerService(serviceInfo);

            this.service = serviceInfo;
        }

        public void close() {
            unregisterService();
            zeroConf.close();
        }
    }

    /**
     * A simple wrapper for a two way mapping as we have to
     * deal in both directions with FileManager and DaapServer
     * <p>
     * Song -> URN
     * URN -> Song
     */
    private final class SongURNMap {

        private HashMap /* Song -> URN */ songToUrn = new HashMap();
        private HashMap /* URN -> Song */ urnToSong = new HashMap();

        public SongURNMap() {
        }

        public void put(Song song, URN urn) {
            songToUrn.put(song, urn);
            urnToSong.put(urn, song);
        }

        public URN get(Song song) {
            return (URN) songToUrn.get(song);
        }

        public Song get(URN urn) {
            return (Song) urnToSong.get(urn);
        }

        public Song remove(URN urn) {
            Song song = (Song) urnToSong.remove(urn);
            if (song != null)
                songToUrn.remove(song);
            return song;
        }

        public URN remove(Song song) {
            URN urn = (URN) songToUrn.remove(song);
            if (urn != null)
                urnToSong.remove(urn);
            return urn;
        }

        public boolean contains(URN urn) {
            return urnToSong.containsKey(urn);
        }

        public boolean contains(Song song) {
            return songToUrn.containsKey(song);
        }

        public Iterator getSongIterator() {
            return songToUrn.keySet().iterator();
        }

        public Iterator getURNIterator() {
            return urnToSong.keySet().iterator();
        }

        public void clear() {
            urnToSong.clear();
            songToUrn.clear();
        }

        public int size() {
            // NOTE: songToUrn.size() == urnToSong.size()
            return songToUrn.size();
        }
    }

    private static class ServerUpdater implements TransactionListener {
        private DaapServer server;

        private ServerUpdater(DaapServer server) {
            this.server = server;
        }

        public void commit(Transaction arg0) {
            if (server != null) {
                server.update();
            }
        }

        public void rollback(Transaction arg0) {
        }
    }
}