/*
* Jajuk
* Copyright (C) The Jajuk Team
* http://jajuk.info
*
* 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 2
* of the License, or 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
package org.jajuk.base;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.jajuk.services.core.PersistenceService;
import org.jajuk.services.core.PersistenceService.Urgency;
import org.jajuk.services.tags.Tag;
import org.jajuk.services.webradio.WebRadio;
import org.jajuk.util.Const;
import org.jajuk.util.MD5Processor;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.log.Log;
/**
* Managers parent class.
*/
public abstract class ItemManager {
/** Maps item classes -> instance, must be a linked map for ordering (mandatory in commited collection). */
private static Map<Class<?>, ItemManager> hmItemManagers = new LinkedHashMap<Class<?>, ItemManager>(
10);
/** Maps properties meta information name and object. */
private final Map<String, PropertyMetaInformation> hmPropertiesMetaInformation = new LinkedHashMap<String, PropertyMetaInformation>(
10);
/** The Lock. */
ReadWriteLock lock = new ReentrantReadWriteLock();
/** Use an array list during startup which is faster during loading the collection. */
private List<Item> startupItems = new ArrayList<Item>(100);
/** Stores the items by ID to have quick access if necessary. */
private final Map<String, Item> internalMap = new HashMap<String, Item>(100);
/** Collection pointer : at the beginning point to the ArrayList, later this is replaced by a TreeSet to have correct ordering. */
private Collection<Item> items = startupItems;
/**
* Item manager default constructor.
*/
public ItemManager() {
}
/**
* Switch all item managers to ordered mode See
* ItemManager.switchToOrderState() for more details
*/
public static void switchAllManagersToOrderState() {
Log.debug("Switching to sorted mode");
for (ItemManager manager : hmItemManagers.values()) {
manager.switchToOrderState();
}
}
/**
* Switch this item manager to order mode This feature allows faster
* collection loading As collection.xml contains ordered elements, we simply a
* use an ArrayList to store items first, then few seconds after startup and
* before user could make changes to the collection, we populate a TreeSet
* from the ArrayList and begin to use it.
*/
public void switchToOrderState() {
lock.writeLock().lock();
try {
// populate a new TreeSet with the startup-items
if (startupItems != null) {
items = new TreeSet<Item>(startupItems);
// Free startup memory
startupItems = null;
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Registers a new item manager.
*
* @param c Managed item class
* @param itemManager
*/
public static void registerItemManager(Class<?> c, ItemManager itemManager) {
hmItemManagers.put(c, itemManager);
}
/**
* Gets the XML tag.
*
* @return identifier used for XML generation
*/
public abstract String getXMLTag();
/**
* Gets the meta information.
*
* @param sPropertyName
*
* @return meta data for given property
*/
public PropertyMetaInformation getMetaInformation(String sPropertyName) {
return hmPropertiesMetaInformation.get(sPropertyName);
}
/**
* Remove a property *.
*
* @param sProperty
*/
public void removeProperty(String sProperty) {
PropertyMetaInformation meta = getMetaInformation(sProperty);
hmPropertiesMetaInformation.remove(sProperty);
applyRemoveProperty(meta); // remove this property from all items
}
/**
* Remove a custom property from all items for the given manager.
*
* @param meta
*/
void applyRemoveProperty(PropertyMetaInformation meta) {
lock.readLock().lock();
try {
for (Item item : items) {
item.removeProperty(meta.getName());
}
} finally {
lock.readLock().unlock();
}
}
/**
* Generic method to access to a parameterized list of items.
*
* @param meta
*
* @return the item-parameterized list
*
* protected abstract HashMap<String, Item> getItemsMap();
*/
/** Add a custom property to all items for the given manager */
public void applyNewProperty(PropertyMetaInformation meta) {
lock.readLock().lock();
try {
for (Item item : items) {
item.setProperty(meta.getName(), meta.getDefaultValue());
}
} finally {
lock.readLock().unlock();
}
}
/**
* Attention, this method does not return a full XML, but rather an excerpt
* that is then completed in Collection.commit()!
*
* @return (partial) XML representation of this manager
*/
String toXML() {
StringBuilder sb = new StringBuilder("<").append(getXMLTag() + ">");
Iterator<String> it = hmPropertiesMetaInformation.keySet().iterator();
while (it.hasNext()) {
String sProperty = it.next();
PropertyMetaInformation meta = hmPropertiesMetaInformation.get(sProperty);
sb.append('\n' + meta.toXML());
}
return sb.append('\n').toString();
}
/**
* Return the associated read write lock.
*
* @return the associated read write lock
*/
public ReadWriteLock getLock() {
return lock;
}
/**
* Format the item name to be normalized :
* <p>
* -no underscores or other non-ascii characters
* <p>
* -no spaces at the begin and the end
* <p>
* -All in lower case expect first letter of first word
* <p>
* example: "My artist".
*
* @param sName The name to format.
*
* @return the string
*
* TODO: the "all lowercase" part is not done currently, should this be changed??
*/
public static String format(String sName) {
String sOut;
sOut = sName.trim(); // suppress spaces at the begin and the end
sOut = sOut.replace('-', ' '); // move - to space
sOut = sOut.replace('_', ' '); // move _ to space
char c = sOut.charAt(0);
StringBuilder sb = new StringBuilder(sOut);
sb.setCharAt(0, Character.toUpperCase(c));
return sb.toString();
}
/**
* Gets the properties.
*
* @return properties Meta informations
*/
public Collection<PropertyMetaInformation> getProperties() {
return hmPropertiesMetaInformation.values();
}
/**
* Gets the custom properties including activated extra tags.
*
* @return custom properties Meta informations
*/
public Collection<PropertyMetaInformation> getCustomProperties() {
List<PropertyMetaInformation> col = new ArrayList<PropertyMetaInformation>();
Iterator<PropertyMetaInformation> it = hmPropertiesMetaInformation.values().iterator();
while (it.hasNext()) {
PropertyMetaInformation meta = it.next();
if (meta.isCustom()) {
col.add(meta);
}
}
return col;
}
/**
* Gets the custom properties without the activated extra tags.
*
* @return custom properties Meta informations
*/
public Collection<PropertyMetaInformation> getUserCustomProperties() {
List<PropertyMetaInformation> col = new ArrayList<PropertyMetaInformation>();
Iterator<PropertyMetaInformation> it = hmPropertiesMetaInformation.values().iterator();
while (it.hasNext()) {
PropertyMetaInformation meta = it.next();
if (meta.isCustom() && !Tag.getActivatedExtraTags().contains(meta.getName())) {
col.add(meta);
}
}
return col;
}
/**
* Gets the visible properties.
*
* @return visible properties Meta informations
*/
public Collection<PropertyMetaInformation> getVisibleProperties() {
List<PropertyMetaInformation> col = new ArrayList<PropertyMetaInformation>();
Iterator<PropertyMetaInformation> it = hmPropertiesMetaInformation.values().iterator();
while (it.hasNext()) {
PropertyMetaInformation meta = it.next();
if (meta.isVisible()) {
col.add(meta);
}
}
return col;
}
/**
* Get the manager from a given attribute name.
*
* @param sProperty The property to compare.
*
* @return an ItemManager if one is found for the property or null if none
* found.
*/
public static ItemManager getItemManager(String sProperty) {
if (Const.XML_DEVICE.equals(sProperty)) {
return DeviceManager.getInstance();
} else if (Const.XML_TRACK.equals(sProperty)) {
return TrackManager.getInstance();
} else if (Const.XML_ALBUM.equals(sProperty)) {
return AlbumManager.getInstance();
} else if (Const.XML_ARTIST.equals(sProperty)) {
return ArtistManager.getInstance();
} else if (Const.XML_ALBUM_ARTIST.equals(sProperty)) {
return AlbumArtistManager.getInstance();
} else if (Const.XML_GENRE.equals(sProperty)) {
return GenreManager.getInstance();
} else if (Const.XML_DIRECTORY.equals(sProperty)) {
return DirectoryManager.getInstance();
} else if (Const.XML_FILE.equals(sProperty)) {
return FileManager.getInstance();
} else if (Const.XML_PLAYLIST_FILE.equals(sProperty)) {
return PlaylistManager.getInstance();
} else if (Const.XML_TYPE.equals(sProperty)) {
return TypeManager.getInstance();
} else {
return null;
}
}
/**
* Get ItemManager manager for given item class.
*
* @param c
*
* @return associated item manager or null if none was found
*/
public static ItemManager getItemManager(Class<?> c) {
return hmItemManagers.get(c);
}
/**
* Perform cleanup : delete useless items.
*/
@SuppressWarnings("unchecked")
public void cleanup() {
lock.writeLock().lock();
try {
// Prefetch item manager type for performances
short managerType = 0; // Album
if (this instanceof ArtistManager) {
managerType = 1;
} else if (this instanceof GenreManager) {
Log.debug("Genre cleanup not allowed");
return;
} else if (this instanceof YearManager) {
managerType = 2;
} else if (this instanceof AlbumArtistManager) {
managerType = 3;
}
// build used items set
List<Item> lItems = new ArrayList<Item>(100);
List<Track> tracks = TrackManager.getInstance().getTracks();
for (Track track : tracks) {
switch (managerType) {
case 0:
lItems.add(track.getAlbum());
break;
case 1:
lItems.add(track.getArtist());
break;
case 2:
lItems.add(track.getYear());
break;
case 3:
lItems.add(track.getAlbumArtist());
break;
}
}
// Now iterate over this manager items to check if it is present in the
// items list
Iterator<Item> it = (Iterator<Item>) getItemsIterator();
while (it.hasNext()) {
Item item = it.next();
// check if this item still maps some tracks
if (!lItems.contains(item)) {
it.remove();
internalMap.remove(item.getID());
}
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Perform a cleanup of all orphan tracks associated with given item.
*
* @param item item whose associated tracks should be checked for cleanup
*/
protected void cleanOrphanTracks(Item item) {
if (TrackManager.getInstance().getAssociatedTracks(item, false).isEmpty()) {
removeItem(item);
}
}
/**
* Remove a given item.
*
* @param item
*/
public void removeItem(Item item) {
lock.writeLock().lock();
try {
if (item != null) {
items.remove(item);
internalMap.remove(item.getID());
notifyCollectionChange(item);
}
} finally {
lock.writeLock().unlock();
}
}
private final void notifyCollectionChange(Item item) {
// Ignore this if the persistence service is not yet started to speed up startup
if (!PersistenceService.getInstance().isAlive()) {
return;
}
// SmartPlaylist are not persisted
if (item instanceof SmartPlaylist) {
return;
}
// Webradios are stored outside the collection file and are persisted separately
if (item instanceof WebRadio) {
PersistenceService.getInstance().setRadiosChanged();
} else {
PersistenceService.getInstance().setCollectionChanged(Urgency.HIGH);
}
}
/**
* Register a given item.
*
* @param item : the item to add
*/
protected void registerItem(Item item) {
lock.writeLock().lock();
try {
items.add(item);
internalMap.put(item.getID(), item);
notifyCollectionChange(item);
} finally {
lock.writeLock().unlock();
}
}
/**
* Register a new property.
*
* @param meta
*/
public void registerProperty(PropertyMetaInformation meta) {
hmPropertiesMetaInformation.put(meta.getName(), meta);
}
/**
* Change any item.
*
* @param itemToChange
* @param sKey
* @param oValue
* @param filter : files we want to deal with
*
* @return the changed item
*
* @throws JajukException the jajuk exception
*/
public static Item changeItem(Item itemToChange, String sKey, Object oValue, Set<File> filter)
throws JajukException {
if (Log.isDebugEnabled()) {
Log.debug("Set " + sKey + "=" + oValue.toString() + " to " + itemToChange);
}
Item newItem = itemToChange;
if (itemToChange instanceof File) {
File file = (File) itemToChange;
if (Const.XML_NAME.equals(sKey)) { // file name
newItem = FileManager.getInstance().changeFileName((File) itemToChange, (String) oValue);
} else if (Const.XML_TRACK.equals(sKey)) { // track name
newItem = TrackManager.getInstance().changeTrackName(file.getTrack(), (String) oValue,
filter);
} else if (Const.XML_GENRE.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackGenre(file.getTrack(), (String) oValue,
filter);
} else if (Const.XML_ALBUM.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackAlbum(file.getTrack(), (String) oValue,
filter);
} else if (Const.XML_ARTIST.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackArtist(file.getTrack(), (String) oValue,
filter);
} else if (Const.XML_TRACK_COMMENT.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackComment(file.getTrack(), (String) oValue,
filter);
} else if (Const.XML_TRACK_ORDER.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackOrder(file.getTrack(), (Long) oValue,
filter);
} else if (Const.XML_ALBUM_ARTIST.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackAlbumArtist(file.getTrack(),
(String) oValue, filter);
} else if (Const.XML_YEAR.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackYear(file.getTrack(),
String.valueOf(oValue), filter);
} else if (Const.XML_TRACK_RATE.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackRate(file.getTrack(), (Long) oValue);
} else { // others properties
// check extra tags
if (Tag.getActivatedExtraTags().contains(sKey)) {
TrackManager.getInstance().changeTrackField(file.getTrack(), sKey, (String) oValue,
filter);
}
// check if this key is known for files
if (file.getMeta(sKey) != null) {
itemToChange.setProperty(sKey, oValue);
}
// Unknown ? check if it is a track custom property
else if (file.getTrack().getMeta(sKey) != null) {
file.getTrack().setProperty(sKey, oValue);
}
}
// Get associated track file
if (newItem instanceof Track) {
file.setTrack((Track) newItem);
newItem = file;
}
} else if (itemToChange instanceof Playlist) {
if (Const.XML_NAME.equals(sKey)) { // playlistfile name
newItem = PlaylistManager.getInstance().changePlaylistFileName((Playlist) itemToChange,
(String) oValue);
}
} else if (itemToChange instanceof Directory) {
if (!Const.XML_NAME.equals(sKey)) { // file name
// TBI newItem =
// DirectoryManager.getInstance().changeDirectoryName((Directory)itemToChange,(String)oValue);
// } else { // others properties
itemToChange.setProperty(sKey, oValue);
}
} else if (itemToChange instanceof Device) {
itemToChange.setProperty(sKey, oValue);
} else if (itemToChange instanceof Track) {
if (Const.XML_NAME.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackName((Track) itemToChange, (String) oValue,
filter);
} else if (Const.XML_GENRE.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackGenre((Track) itemToChange,
(String) oValue, filter);
} else if (Const.XML_ALBUM.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackAlbum((Track) itemToChange,
(String) oValue, filter);
} else if (Const.XML_ARTIST.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackArtist((Track) itemToChange,
(String) oValue, filter);
} else if (Const.XML_ALBUM_ARTIST.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackAlbumArtist((Track) itemToChange,
(String) oValue, filter);
} else if (Const.XML_TRACK_COMMENT.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackComment((Track) itemToChange,
(String) oValue, filter);
} else if (Const.XML_TRACK_ORDER.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackOrder((Track) itemToChange, (Long) oValue,
filter);
} else if (Const.XML_YEAR.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackYear((Track) itemToChange,
String.valueOf(oValue), filter);
} else if (Const.XML_TRACK_RATE.equals(sKey)) {
newItem = TrackManager.getInstance().changeTrackRate((Track) itemToChange, (Long) oValue);
} else { // others properties
itemToChange.setProperty(sKey, oValue);
}
} else if (itemToChange instanceof Album) {
if (Const.XML_NAME.equals(sKey)) {
newItem = AlbumManager.getInstance().changeAlbumName((Album) itemToChange, (String) oValue);
} else { // others properties
itemToChange.setProperty(sKey, oValue);
}
} else if (itemToChange instanceof Artist) {
if (Const.XML_NAME.equals(sKey)) {
newItem = ArtistManager.getInstance().changeArtistName((Artist) itemToChange,
(String) oValue);
} else { // others properties
itemToChange.setProperty(sKey, oValue);
}
} else if (itemToChange instanceof Genre) {
if (Const.XML_NAME.equals(sKey)) {
newItem = GenreManager.getInstance().changeGenreName((Genre) itemToChange, (String) oValue);
} else { // others properties
itemToChange.setProperty(sKey, oValue);
}
} else if (itemToChange instanceof Year) {
itemToChange.setProperty(sKey, oValue);
} else if (itemToChange instanceof WebRadio) {
itemToChange.setProperty(sKey, oValue);
if (Const.XML_NAME.equals(sKey)) {
itemToChange.setName((String) oValue);
}
}
return newItem;
}
/**
* Gets the element count.
*
* @return number of item
*/
public int getElementCount() {
return items.size();
}
/**
* Gets the item by id.
*
* @param sID Item ID
*
* @return Item
*/
public Item getItemByID(String sID) {
return internalMap.get(sID);
}
/**
* Return a copy of all registered items. The resulting list can be used without
* need of locking.
*
* @return a copy of all registered items
*/
public List<? extends Item> getItems() {
// getItems() creates a copy of the list of items and thus iterates over the current list of items
// therefore a ConcurrentModifcationException could be triggered if we do not lock while actually
// doing the copying. Usage of the list afterwards is save without locking
lock.readLock().lock();
try {
return new ArrayList<Item>(items);
} finally {
lock.readLock().unlock();
}
}
/**
* ***************************************************************************
* Return all registered enumeration CAUTION : do not call remove() on this
* iterator, you would effectively remove items instead of using regular
* removeItem() primitive
* **************************************************************************.
*
* @return the items iterator
*/
protected Iterator<? extends Item> getItemsIterator() {
return items.iterator();
}
/**
* Clear any entries from this manager.
*/
public void clear() {
lock.writeLock().lock();
try {
items.clear();
internalMap.clear();
} finally {
lock.writeLock().unlock();
}
}
/**
* Force files sorting after an order change, i.e. Called to ensure Set
* sorting contract <br>
* We remove all items and add them all again to force sorting
*/
public void forceSorting() {
lock.writeLock().lock();
try {
// first create a copy
ArrayList<Item> itemsCopy = new ArrayList<Item>(items);
// then remove all elements
clear();
// and then re-add all items again to make them correctly sorted again
for (Item item : itemsCopy) {
registerItem(item);
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Basic implementation for item hashcode computation.
*
* @param sName item name
*
* @return ItemManager ID
*/
protected static String createID(String sName) {
return MD5Processor.hash(sName);
}
}