/*
You may freely copy, distribute, modify and use this class as long
as the original author attribution remains intact. See message
below.
Copyright (C) 2003 Christian Pesch. All Rights Reserved.
*/
package slash.metamusic.itunes.xml;
import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;
import org.xml.sax.ContentHandler;
import slash.metamusic.itunes.xml.binding.Array;
import slash.metamusic.itunes.xml.binding.Dict;
import slash.metamusic.itunes.xml.binding.ObjectFactory;
import slash.metamusic.itunes.xml.util.JaxbUtils;
import javax.xml.bind.*;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
public class iTunesXMLLibrary {
public static final JAXBContext CONTEXT = JaxbUtils.newContext(ObjectFactory.class);
public static final Source[] SCHEMAS = {
new StreamSource(JaxbUtils.getResource(iTunesXMLLibrary.class, "schemas/xml.xsd")),
};
public static Unmarshaller newUnmarshaller() {
return JaxbUtils.newUnmarshaller(CONTEXT);
}
public static Marshaller newMarshaller() {
return JaxbUtils.newMarshaller(CONTEXT
/* "http://www.apple.com/DTDs/PropertyList-1.0.dtd", "pl", */
/* "http://www.w3.org/2001/XMLSchema-instance", "xsi" */
);
}
public static <T> void marshal(String uri, String localName, Class<T> c, T o, OutputStream out) throws JAXBException {
// work around iTunes bug: xml declaration has to exist and declare UTF-8, but content is encoded with systems local encoding
try {
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".getBytes("ISO8859-1"));
} catch (IOException e) {
throw new JAXBException("Could not write xml declaration: " + e.getMessage(), e);
}
Marshaller marshaller = newMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, System.getProperty("file.encoding"));
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
marshaller.marshal(new JAXBElement<T>(new QName(uri, localName), c, o), out);
}
public static <T> void marshal(String uri, String localName, Class<T> c, T o, ContentHandler out) throws JAXBException {
newMarshaller().marshal(new JAXBElement<T>(new QName(uri, localName), c, o), out);
}
public static <T> Object unmarshal(Class<T> c, Source source) throws JAXBException {
return newUnmarshaller().unmarshal(source, c).getValue();
}
public static Map<String, Object> dictToMap(Dict dict) {
Map<String, Object> result = new HashMap<String, Object>();
List<Object> objects = dict.getKeyAndArrayOrData();
for (int i = 0; i < objects.size(); i += 2) {
JAXBElement key = (JAXBElement) objects.get(i);
Object value = objects.get(i + 1);
if (value instanceof JAXBElement)
value = ((JAXBElement) value).getValue();
result.put(key.getValue().toString(), value);
}
return result;
}
public static List<Object> arrayToList(Array array) {
List<Object> result = new ArrayList<Object>();
if (array != null)
result.addAll(array.getArrayOrDataOrDate());
return result;
}
protected static class Dictionary {
private Dict delegate;
public Dictionary(Dict value) {
this.delegate = value;
}
protected Dict getDelegate() {
return delegate;
}
public Object get(String key) {
List<Object> elements = delegate.getKeyAndArrayOrData();
for (int i = 0; i < elements.size(); i += 2) {
JAXBElement keyElement = (JAXBElement) elements.get(i);
String foundKey = (String) keyElement.getValue();
if (key.equals(foundKey)) {
Object value = elements.get(i + 1);
if (value instanceof JAXBElement)
return ((JAXBElement) value).getValue();
else if (value instanceof Array)
return ((Array) value).getArrayOrDataOrDate();
else
throw new IllegalArgumentException("Type " + value.getClass() + " not supported");
}
}
return null;
}
public void put(String key, Object value) {
List<Object> elements = this.delegate.getKeyAndArrayOrData();
for (int i = 0; i < elements.size(); i += 2) {
JAXBElement keyElement = (JAXBElement) elements.get(i);
String foundKey = (String) keyElement.getValue();
if (key.equals(foundKey)) {
JAXBElement valueElement = (JAXBElement) elements.get(i + 1);
valueElement.setValue(value);
return;
}
}
ObjectFactory objectFactory = new ObjectFactory();
elements.add(objectFactory.createKey(key));
if (value instanceof BigInteger)
elements.add(objectFactory.createInteger((BigInteger) value));
else if (value instanceof XMLGregorianCalendar)
elements.add(objectFactory.createDate((XMLGregorianCalendar) value));
else
throw new IllegalArgumentException("Type " + value + " not supported");
}
public String toString() {
List<Object> elements = delegate.getKeyAndArrayOrData();
return super.toString() + " " + elements;
}
}
public static class Track extends Dictionary {
private Dict parent;
private JAXBElement key;
public Track(Dict parent, JAXBElement key, Dict value) {
super(value);
this.parent = parent;
this.key = key;
}
public int getId() {
BigInteger rating = (BigInteger) get("Track ID");
return rating != null ? rating.intValue() : null;
}
public Integer getRating() {
BigInteger rating = (BigInteger) get("Rating");
return rating != null ? rating.intValue() : null;
}
public void setRating(int rating) {
put("Rating", new BigInteger(Integer.toString(rating)));
}
public Integer getPlayCount() {
BigInteger playCount = (BigInteger) get("Play Count");
return playCount != null ? playCount.intValue() : null;
}
public void setPlayCount(int playCount) {
put("Play Count", new BigInteger(Integer.toString(playCount)));
}
public Calendar getPlayTime() {
XMLGregorianCalendar playDate = (XMLGregorianCalendar) get("Play Date UTC");
if (playDate == null)
return null;
Calendar calendar = playDate.toGregorianCalendar();
// convert UTC to default time zone
calendar.setTimeZone(TimeZone.getDefault());
return calendar;
}
public void setPlayTime(Calendar playTime) {
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(playTime.getTime());
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
// convert default time zone time to UTC
int offset = TimeZone.getDefault().getOffset(playTime.getTimeInMillis());
calendar.add(Calendar.MILLISECOND, offset);
put("Play Date UTC", new XMLGregorianCalendarImpl(calendar));
put("Play Date", new BigInteger(Long.toString(calendar.getTimeInMillis())));
}
public Calendar getModificationTime() {
XMLGregorianCalendar modificationDate = (XMLGregorianCalendar) get("Date Modified");
if (modificationDate == null)
return null;
return modificationDate.toGregorianCalendar();
}
public Calendar getSynchronizationTime() {
XMLGregorianCalendar synchronizationDate = (XMLGregorianCalendar) get("Date Synced");
if (synchronizationDate == null)
return null;
return synchronizationDate.toGregorianCalendar();
}
public void setSynchronizationTime(Calendar synchronizationTime) {
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(synchronizationTime.getTime());
put("Date Synced", new XMLGregorianCalendarImpl(calendar));
}
public URL getLocation() {
String location = (String) get("Location");
try {
if (location != null) {
location = location.replaceAll("\\+", "%2B");
location = URLDecoder.decode(location, "UTF-8");
return new URL(location);
}
} catch (IOException e) {
// intentionally left empty
}
return null;
}
public boolean delete() {
List<Object> data = parent.getKeyAndArrayOrData();
return data.remove(key) && data.remove(getDelegate());
}
}
public static class Playlist extends Dictionary {
public Playlist(Dict value) {
super(value);
}
public String getName() {
return (String) get("Name");
}
public int delete(Set<Track> accessibleTracks) {
List<Object> items = (List<Object>) get("Playlist Items");
if (items == null)
return -1;
List<Object> removedItems = new ArrayList<Object>();
for (Object item : items) {
Dict dict = (Dict) item;
Map<String, Object> map = dictToMap(dict);
int trackId = ((BigInteger) map.get("Track ID")).intValue();
if (findTrackById(accessibleTracks, trackId) == null) {
removedItems.add(item);
}
}
for (Object removedItem : removedItems) {
items.remove(removedItem);
}
return removedItems.size();
}
private Track findTrackById(Set<Track> tracks, int trackId) {
for (Track track : tracks) {
if (track.getId() == trackId)
return track;
}
return null;
}
}
}