/* * Digital Audio Access Protocol (DAAP) Library * Copyright (C) 2004-2010 Roger Kapsi * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ardverk.daap; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import org.ardverk.daap.chunks.ContentCodesResponseImpl; import org.ardverk.daap.chunks.impl.AuthenticationMethod; import org.ardverk.daap.chunks.impl.AuthenticationSchemes; import org.ardverk.daap.chunks.impl.ContainerCount; import org.ardverk.daap.chunks.impl.ContentCodesResponse; import org.ardverk.daap.chunks.impl.DaapProtocolVersion; import org.ardverk.daap.chunks.impl.DatabaseCount; import org.ardverk.daap.chunks.impl.DeletedIdListing; import org.ardverk.daap.chunks.impl.DmapProtocolVersion; import org.ardverk.daap.chunks.impl.ItemCount; import org.ardverk.daap.chunks.impl.ItemId; import org.ardverk.daap.chunks.impl.ItemName; import org.ardverk.daap.chunks.impl.Listing; import org.ardverk.daap.chunks.impl.ListingItem; import org.ardverk.daap.chunks.impl.LoginRequired; import org.ardverk.daap.chunks.impl.LoginResponse; import org.ardverk.daap.chunks.impl.PersistentId; import org.ardverk.daap.chunks.impl.ReturnedCount; import org.ardverk.daap.chunks.impl.ServerDatabases; import org.ardverk.daap.chunks.impl.ServerInfoResponse; import org.ardverk.daap.chunks.impl.ServerRevision; import org.ardverk.daap.chunks.impl.SpecifiedTotalCount; import org.ardverk.daap.chunks.impl.Status; import org.ardverk.daap.chunks.impl.SupportsBrowse; import org.ardverk.daap.chunks.impl.SupportsExtensions; import org.ardverk.daap.chunks.impl.SupportsIndex; import org.ardverk.daap.chunks.impl.SupportsPersistentIds; import org.ardverk.daap.chunks.impl.SupportsQuery; import org.ardverk.daap.chunks.impl.SupportsUpdate; import org.ardverk.daap.chunks.impl.TimeoutInterval; import org.ardverk.daap.chunks.impl.UpdateResponse; import org.ardverk.daap.chunks.impl.UpdateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Roger Kapsi */ public class Library { private static final Logger LOG = LoggerFactory.getLogger(Library.class); private static final AtomicLong PERISTENT_ID = new AtomicLong(1); /** The revision of this Library */ private int revision = 0; /** Name of this Library */ private String name; /** The total number of Databases in this Library */ private int totalDatabaseCount = 0; /** Set of Databases */ private final Set<Database> databases = new HashSet<Database>(); /** Set of deleted Databases */ private Set<Database> deletedDatabases = null; /** List of listener */ private final List<WeakReference<LibraryListener>> listener = new ArrayList<WeakReference<LibraryListener>>(); protected boolean clone = false; protected Library(Library library, Transaction txn) { this.name = library.name; this.revision = library.revision; if (library.deletedDatabases != null) { this.deletedDatabases = library.deletedDatabases; library.deletedDatabases = null; } for (Database database : library.databases) { if (txn.modified(database)) { if (deletedDatabases == null || !deletedDatabases.contains(database)) { Database clone = new Database(database, txn); databases.add(clone); } } } this.totalDatabaseCount = library.totalDatabaseCount; this.clone = true; init(); } public Library(String name) { this.name = name; commit(null); init(); } private void init() { } /** * Returns the current revision of this library. */ public synchronized int getRevision() { return revision; } /** * Sets the name of this Library. Note: Library must be open or an * <tt>IllegalStateException</tt> will be thrown */ public void setName(Transaction txn, final String name) { if (txn != null) { txn.addTxn(this, new Txn() { public void commit(Transaction txn) { setNameP(txn, name); } }); } else { setNameP(txn, name); } } private void setNameP(Transaction txn, String name) { this.name = name; } /** * Returns the name of this Library */ public String getName() { return name; } /** * * @return */ public Set<Database> getDatabases() { return Collections.unmodifiableSet(databases); } /** * Adds database to this Library (<b>NOTE</b>: only one Database per Library * is supported by iTunes!) * * @param database * @throws DaapTransactionException */ public void addDatabase(Transaction txn, final Database database) { if (txn != null) { txn.addTxn(this, new Txn() { public void commit(Transaction txn) { addDatabaseP(txn, database); } }); txn.attach(database); } else { addDatabaseP(txn, database); } } private void addDatabaseP(Transaction txn, Database database) { if (!databases.isEmpty()) { throw new DaapException("One Database per Library is maximum."); } if (databases.add(database)) { totalDatabaseCount = databases.size(); if (deletedDatabases != null && deletedDatabases.remove(database) && deletedDatabases.isEmpty()) { deletedDatabases = null; } } } /** * Removes database from this Library * * @param database * @throws DaapTransactionException */ public void removeDatabase(Transaction txn, final Database database) { if (txn != null) { txn.addTxn(this, new Txn() { public void commit(Transaction txn) { removeDatabaseP(txn, database); } }); } else { removeDatabaseP(txn, database); } } private void removeDatabaseP(Transaction txn, Database database) { if (databases.remove(database)) { totalDatabaseCount = databases.size(); if (deletedDatabases == null) { deletedDatabases = new HashSet<Database>(); } deletedDatabases.add(database); } } /** * Returns true if this Library contains database * * @param database * @return */ public synchronized boolean containsDatabase(Database database) { return databases.contains(database); } public synchronized Transaction beginTransaction() { Transaction txn = new Transaction(this); return txn; } /** * Returns some kind of Object or null if <tt>request</tt> didn't matched * for this Library (unknown request, unknown id, whatever). The returned * Object could be basically anything but it's in our case either an * <tt>java.lang.Integer</tt> or a byte-Array (gzip'ed). */ public synchronized Object select(DaapRequest request) { // System.out.println("REQUEST: " + request); if (request.isServerInfoRequest()) { return getServerInfo(request); } else if (request.isLoginRequest()) { return getLoginResponse(request); } else if (request.isContentCodesRequest()) { return getContentCodes(request); } else if (request.isUpdateRequest()) { return getUpdateResponse(request); } else if (request.isDatabasesRequest()) { return getServerDatabases(request); } else if (request.isSongRequest() || request.isDatabaseSongsRequest() || request.isDatabasePlaylistsRequest() || request.isPlaylistSongsRequest()) { Database database = getDatabase(request); if (database == null) { if (LOG.isInfoEnabled()) { LOG.info("No database with this revision known: " + request.getRevisionNumber()); } return null; } return database.select(request); } else { if (LOG.isInfoEnabled()) { LOG.info("Unknown request: " + request); } return null; } } public synchronized void commit(Transaction txn) { if (txn == null) { txn = new Transaction(this); txn.addTxn(this, new Txn()); txn.commit(); return; } this.revision++; Library diff = new Library(this, txn); synchronized (listener) { Iterator<WeakReference<LibraryListener>> it = listener.iterator(); while (it.hasNext()) { LibraryListener l = it.next().get(); if (l == null) { it.remove(); } else { l.libraryChanged(this, diff); } } } } protected synchronized void rollback(Transaction txn) { // TODO: add code, actually do nothing... } protected synchronized void close(Transaction txn) { } /** * Returns the number of Databases * * @return */ public int getDatabaseCount() { return databases.size(); } @Override public String toString() { if (!clone) { return "Library(" + revision + ")"; } else { return "LibraryPatch(" + revision + ")"; } } protected static long nextPersistentId() { return PERISTENT_ID.getAndIncrement(); } public void addLibraryListener(LibraryListener l) { synchronized (listener) { listener.add(new WeakReference<LibraryListener>(l)); } } public void removeLibraryListener(LibraryListener l) { synchronized (listener) { Iterator<WeakReference<LibraryListener>> it = listener.iterator(); while (it.hasNext()) { LibraryListener gotten = it.next().get(); if (gotten == null || gotten == l) { it.remove(); } } } } protected Database getDatabase(DaapRequest request) { long databaseId = request.getDatabaseId(); for (Database database : databases) { if (database.getItemId() == databaseId) { return database; } } return null; } private LoginResponse getLoginResponse(DaapRequest request) { SessionId sessionId = request.getSessionId(); if (sessionId.equals(SessionId.INVALID)) { if (LOG.isErrorEnabled()) { LOG.error("Unknown SessionId, check Server code!"); } return null; } LoginResponse loginResponse = new LoginResponse(); loginResponse.add(new Status(200)); loginResponse.add(new org.ardverk.daap.chunks.impl.SessionId(sessionId .intValue())); return loginResponse; } private ContentCodesResponse getContentCodes(DaapRequest request) { return new ContentCodesResponseImpl(); } private UpdateResponse getUpdateResponse(DaapRequest request) { UpdateResponse updateResponse = new UpdateResponse(); updateResponse.add(new Status(200)); updateResponse.add(new ServerRevision(getRevision())); return updateResponse; } private ServerDatabases getServerDatabases(DaapRequest request) { ServerDatabases serverDatabases = new ServerDatabases(); serverDatabases.add(new Status(200)); serverDatabases.add(new UpdateType(request.isUpdateType() ? 1 : 0)); serverDatabases.add(new SpecifiedTotalCount(totalDatabaseCount)); serverDatabases.add(new ReturnedCount(databases.size())); Listing listing = new Listing(); for (Database database : databases) { ListingItem listingItem = new ListingItem(); listingItem.add(new ItemId(database.getItemId())); listingItem.add(new PersistentId(database.getPersistentId())); listingItem.add(new ItemName(database.getName())); listingItem.add(new ItemCount(database.getSongCount())); listingItem.add(new ContainerCount(database.getPlaylistCount())); listing.add(listingItem); } serverDatabases.add(listing); if (request.isUpdateType() && deletedDatabases != null) { DeletedIdListing deletedListing = new DeletedIdListing(); for (Database database : deletedDatabases) deletedListing.add(new ItemId(database.getItemId())); serverDatabases.add(deletedListing); } return serverDatabases; } private ServerInfoResponse getServerInfo(DaapRequest request) { DaapConnection connection = request.getConnection(); int version = connection.getProtocolVersion(); if (version < DaapUtil.DAAP_VERSION_3) { return null; } DaapServer<?> server = request.getServer(); DaapConfig config = server.getConfig(); ServerInfoResponse serverInfoResponse = new ServerInfoResponse(); serverInfoResponse.add(new Status(200)); serverInfoResponse.add(new TimeoutInterval(1800)); serverInfoResponse.add(new DmapProtocolVersion( DaapUtil.DMAP_VERSION_201)); serverInfoResponse .add(new DaapProtocolVersion(DaapUtil.DAAP_VERSION_3)); // serverInfoResponse.add(new // MusicSharingVersion(DaapUtil.MUSIC_SHARING_VERSION_201)); serverInfoResponse.add(new ItemName(name)); // NOTE: the value of the following boolean chunks does not matter! // They are either present (=true) or not (=false). // client should perform /login request (create session) serverInfoResponse.add(new LoginRequired(true)); serverInfoResponse.add(new SupportsBrowse(true)); serverInfoResponse.add(new SupportsPersistentIds(false)); serverInfoResponse.add(new SupportsIndex(true)); serverInfoResponse.add(new SupportsQuery(true)); serverInfoResponse.add(new SupportsUpdate(true)); // serverInfoResponse.add(new SupportsAutoLogout(true)); // TODO: figure out what is an extension and what not. // /content-codes request serverInfoResponse.add(new SupportsExtensions(true)); /* * serverInfoResponse.add(new SupportsBrowse(true)); * serverInfoResponse.add(new SupportsQuery(true)); * serverInfoResponse.add(new SupportsIndex(true)); * serverInfoResponse.add(new SupportsResolve(true)); */ Object authenticationMethod = config.getAuthenticationMethod(); if (!authenticationMethod.equals(DaapConfig.NO_PASSWORD)) { if (authenticationMethod.equals(DaapConfig.PASSWORD)) { serverInfoResponse.add(new AuthenticationMethod( AuthenticationMethod.PASSWORD_METHOD)); } else { serverInfoResponse.add(new AuthenticationMethod( AuthenticationMethod.USERNAME_PASSWORD_METHOD)); } serverInfoResponse.add(new AuthenticationSchemes( AuthenticationSchemes.BASIC_SCHEME | AuthenticationSchemes.DIGEST_SCHEME)); } serverInfoResponse.add(new DatabaseCount(getDatabaseCount())); return serverInfoResponse; } }