/*
* 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 com.google.common.collect.Lists;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.ImageIcon;
import org.jajuk.services.core.PersistenceService;
import org.jajuk.services.core.PersistenceService.Urgency;
import org.jajuk.services.webradio.WebRadio;
import org.jajuk.util.Const;
import org.jajuk.util.UtilString;
import org.jajuk.util.log.Log;
import org.xml.sax.Attributes;
/**
* Generic property handler, parent class for all items
* <p>
* Note that some properties can be omitted (not in properties object), in this
* case, we take default value given in meta infos, this can decrease collection
* file size
* </p>.
*/
public abstract class Item implements Const {
/** We cache the ID to avoid getting it from properties for CPU performance reasons. */
private String sID;
/** We cache the name to avoid getting it from properties for CPU performance reasons. */
String name;
/** Item properties, singleton use very high load factor as this size will not change often. */
private Map<String, Object> properties = new HashMap<String, Object>(2, 1f);
/** Cache-string which holds the filter-string for the default "any"-Searches, this is filled during the first search and
* cleaned on all points where the properties are adjusted. */
private String any = null;
private static final List<String> lowPriorityCollectionProperties = Lists.asList(XML_TRACK_HITS,
new String[] { XML_TRACK_TOTAL_PLAYTIME, XML_EXPANDED, XML_ALBUM_DISCOVERED_COVER,
XML_TRACK_RATE, XML_ORIGIN });
/**
* Constructor.
*
* @param sId element ID
* @param sName element name
*/
Item(final String sId, final String sName) {
this.sID = sId;
setProperty(Const.XML_ID, sId);
this.name = sName;
setProperty(Const.XML_NAME, sName);
}
/**
* Gets the iD.
*
* @return the iD
*/
public String getID() {
return this.sID;
}
/**
* Gets the name.
*
* @return the name
*/
public String getName() {
return name;
}
/**
* Set a new name
* @param newName
*/
void setName(String newName) {
this.name = newName;
setProperty(XML_NAME, newName);
notifyCollectionChange(XML_NAME);
}
/**
* Item hashcode (used by the equals method) See
* http://www.geocities.com/technofundo/tech/java/equalhash.html
*
* Note that the hashCode is already cached in String class, no need to do it again.
*
* @return the int
*/
@Override
public int hashCode() {
return getID().hashCode();
}
/**
* Get item title (HTML) used in some dialogs.
*
* @return item description
*/
public abstract String getTitle();
/**
* Equal method to check two items are identical.
*
* @param otherItem
*
* @return true, if equals
*/
@Override
public boolean equals(Object otherItem) {
// this also catches null
if (!(otherItem instanceof Item)) {
return false;
}
// [Perf] We can compare with an == operator here because
// all ID are stored into String intern() buffer
return getID() == ((Item) otherItem).getID();
}
/**
* Get a defensive copy of all the item properties.
*
* @return a defensive copy of all the item properties
*/
public Map<String, Object> getProperties() {
return new HashMap<String, Object>(properties);
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.Item#getProperty(java.lang.String)
*/
/**
* Gets the value.
*
* @param sKey
*
* @return the value
*/
public Object getValue(String sKey) {
Object out = properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return getDefaultValue(sKey);
}
return out;
}
/**
* Gets the long value.
*
* @param sKey
*
* @return the long value
*/
public long getLongValue(String sKey) {
Long out = (Long) properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return (Long) getDefaultValue(sKey);
}
return out;
}
/**
* Gets the double value.
*
* @param sKey
*
* @return the double value
*/
public double getDoubleValue(String sKey) {
Double out = (Double) properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return (Double) getDefaultValue(sKey);
}
return out;
}
/**
* Return String value for String type values. We assume that given property
* is a String.
*
* @param sKey
*
* @return the string value
*/
public String getStringValue(String sKey) {
String out = (String) properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return (String) getDefaultValue(sKey);
}
return out;
}
/**
* Gets the boolean value.
*
* @param sKey
*
* @return the boolean value
*/
public boolean getBooleanValue(String sKey) {
Boolean out = (Boolean) properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return (Boolean) getDefaultValue(sKey);
}
return out;
}
/**
* Gets the date value.
*
* @param sKey
*
* @return the date value
*/
public Date getDateValue(String sKey) {
Date out = (Date) properties.get(sKey);
// look at properties to check the given property is known
if (out == null) {
// no? take property default
return (Date) getDefaultValue(sKey);
}
return out;
}
/**
* Gets the default value.
*
* @param sKey
*
* @return the default value
*/
private Object getDefaultValue(String sKey) {
PropertyMetaInformation meta = getMeta(sKey);
return meta.getDefaultValue();
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.Item#containsKey(java.lang.String)
*/
/**
* Contains property.
*
*
* @param sKey
*
* @return true if...
*/
public boolean containsProperty(String sKey) {
return properties.containsKey(sKey) && properties.get(sKey) != null
&& !properties.get(sKey).equals("");
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.Item#setProperty(java.lang.String, java.lang.String)
*/
/**
* Sets the property.
*
*
* @param sKey
* @param oValue
*/
public final void setProperty(String sKey, Object oValue) {
// reset cached value
any = null;
properties.put(sKey, oValue);
notifyCollectionChange(sKey);
}
private final void notifyCollectionChange(String key) {
// Ignore this if the persistence service is not yet started to speed up startup
if (!PersistenceService.getInstance().isAlive()) {
return;
}
// SmartPlaylist are not persisted
if (this instanceof SmartPlaylist) {
return;
}
// Webradios are stored outside the collection file and are persisted separately
if (this instanceof WebRadio) {
PersistenceService.getInstance().setRadiosChanged();
} else {
// Some properties like the track total play time is incremented very often and we don't want to commit
// collection that soon so we set a low urgency to its commit
if (lowPriorityCollectionProperties.contains(key)) {
PersistenceService.getInstance().setCollectionChanged(Urgency.LOW);
} else {
// On the contrary, we want others changes to be commited ASAP
PersistenceService.getInstance().setCollectionChanged(Urgency.HIGH);
}
}
}
/**
* Gets the any.
*
* @return the any
*/
public String getAny() {
if (any != null) {
return any;
}
StringBuilder sb = new StringBuilder(100);
Iterator<String> it = properties.keySet().iterator();
while (it.hasNext()) {
String sKey = it.next();
String sValue = getHumanValue(sKey);
if (sValue != null) {
PropertyMetaInformation meta = getMeta(sKey);
if (!meta.isVisible()) { // computes "any" only on
// visible items
continue;
}
sb.append(sValue);
}
}
any = sb.toString();
return any;
}
/**
* Return an XML representation of this item.
*
* @return the string
*/
String toXml() {
try {
StringBuilder sb = new StringBuilder("<").append(getXMLTag());
sb.append(getPropertiesXml());
sb.append("/>\n");
return sb.toString();
} catch (Exception e) {
// catch any error here because it can prevent
// collection to commit
Log.error(e);
return "";
}
}
/**
* Gets the XML tag.
*
* @return an identifier used to generate XML representation of this item
*/
public abstract String getXMLTag();
/**
* Gets the properties xml.
*
* @return XML representation for item properties
*/
private String getPropertiesXml() {
StringBuilder sb = new StringBuilder();
for (String sKey : properties.keySet()) {
String sValue = null;
Object oValue = properties.get(sKey);
if (oValue != null) {
PropertyMetaInformation meta = getMeta(sKey);
// The meta can be null for unknown reason, see #1226
if (meta == null) {
Log.warn("Null meta for: " + sKey);
continue;
}
try {
sValue = UtilString.format(oValue, meta, false);
// make sure to remove
// non-XML characters
sValue = UtilString.formatXML(sValue);
sb.append(' ');
sb.append(UtilString.formatXML(sKey));
sb.append("='");
sb.append(sValue);
sb.append("'");
} catch (Exception e) { // should not occur
Log.debug("Key=" + sKey + " Meta=" + meta);
Log.error(e);
}
}
}
return sb.toString();
}
/**
* Set all personal properties of an XML file for an item (doesn't overwrite
* existing properties for perfs).
*
* @param attributes :
* list of attributes for this XML item
*/
public void populateProperties(Attributes attributes) {
for (int i = 0; i < attributes.getLength(); i++) {
String sProperty = attributes.getQName(i);
if (!properties.containsKey(sProperty)) {
String sValue = attributes.getValue(i);
PropertyMetaInformation meta = getMeta(sProperty);
try {
if (meta != null) {
// small memory optimization: there are some properties that we do not automatically intern during collection loading,
// therefore do it manually here to not have the strings duplicated.
// This is currently useful for "ALBUM_ARTIST" and for Const.NONE Cover in Albums
// measured gain: aprox. 1MB for 25k tracks
if (Const.XML_ALBUM_ARTIST.equals(sProperty) || Const.COVER_NONE.equals(sValue)) {
setProperty(sProperty, UtilString.parse(sValue.intern(), meta.getType()));
} else {
setProperty(sProperty, UtilString.parse(sValue, meta.getType()));
}
}
} catch (Exception e) {
Log.error(137, sProperty, e);
}
}
}
// remove cached value
any = null;
}
/**
* Sets the properties.
*
* @param properties The properties to set.
*/
public void setProperties(Map<String, Object> properties) {
this.properties = properties;
// remove cached value
any = null;
notifyCollectionChange(null);
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.Item#removeProperty(java.lang.String)
*/
/**
* Removes the property.
*
*
* @param sKey
*/
public void removeProperty(String sKey) {
properties.remove(sKey);
// remove cached value
any = null;
notifyCollectionChange(null);
}
/**
* Default implementation for this method, simply return standard value.
*
* @param sKey
*
* @return the human value
*/
public String getHumanValue(String sKey) {
try {
return UtilString.format(getValue(sKey), getMeta(sKey), true);
} catch (Exception e) {
Log.error(e);
return "";
}
}
/**
* Gets the meta.
*
* @param sProperty Property name
*
* @return Meta information for current item and given property name
*/
public PropertyMetaInformation getMeta(String sProperty) {
return ItemManager.getItemManager(this.getClass()).getMetaInformation(sProperty);
}
/**
* Clone all properties from a given properties list but not overwrite
* constructor properties.
*
* @param propertiesSource
*/
void cloneProperties(Item propertiesSource) {
Iterator<String> it = propertiesSource.getProperties().keySet().iterator();
while (it.hasNext()) {
String sProperty = it.next();
if (!getMeta(sProperty).isConstructor()) {
this.properties.put(sProperty, propertiesSource.getValue(sProperty));
}
}
// reset cached value
any = null;
}
/**
* Gets the icon representation.
*
* @return an icon representation for this item or null if none available
*/
public abstract ImageIcon getIconRepresentation();
/**
* Item rate. Should be overwritten by sub classes
*
* @return item rate if item supports rating or -1 otherwise
*/
public long getRate() {
return -1;
}
}