package se.despotify.client.protocol;
import com.sun.xml.internal.stream.events.XMLEventAllocatorImpl;
import se.despotify.domain.MemoryStore;
import se.despotify.domain.Store;
import se.despotify.domain.media.*;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.XMLEvent;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.*;
/**
* Spotify XML response parser
* <p/>
* Uses XML stream to parse the data.
* <p/>
* <p/>
* Benchmarks compared with previous DOM based solution:
* <p/>
* 5 tracks takes ~30 milliseconds to request gzipped XML.
* ~30 ms mean time to unmarshall 5 tracks using DOM.
* ~7 ms using XML stream
* <p/>
* <p/>
* 200 tracks takes ~120 milliseconds to request gzipped XML.
* ~400 ms mean time to unmarshall 200 tracks using DOM.
* ~50 ms using XML stream
* <p/>
* The average artist takes ~50 ms to request gzipped XML
* while a major artist can take up to one second.
* ~200 ms mean time to unmarshall an artist when loading 200 random
* ~50 ms using XML stream
* <p/>
* Dolly Parton takes 245 ms with XML stream and 4130 ms with DOM.
* Kenny Rogers takes 131 ms with XML stream and 2815 ms with DOM.
* Johnny Cash takes 480 ms with XML stream and 8503 ms with DOM.
*
* @author kalle
* @since 2009-jun-24 00:40:32
*/
public class ResponseUnmarshaller {
/**
* TODO move these to tests
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
loadArtist();
loadAlbum();
loadTracks();
}
public static void loadArtist() throws Exception {
Store store = new MemoryStore();
XMLStreamReader xmlr = createReader(new InputStreamReader(new FileInputStream(new java.io.File("src/main/resources/response/xml/load_artist_4f9873e19e5a4b4096c216c98bcdb010.xml")), "UTF8"));
ResponseUnmarshaller unmarshaller = new ResponseUnmarshaller(store, xmlr);
unmarshaller.skip();
Artist artist = unmarshaller.unmarshallArtist(new Date());
System.currentTimeMillis();
}
public static void loadAlbum() throws Exception {
Store store = new MemoryStore();
XMLStreamReader xmlr = createReader(new InputStreamReader(new FileInputStream(new java.io.File("src/main/resources/response/xml/load_album_2f90dd571f8942e0b8bb6e06b2f6d5ed.xml")), "UTF8"));
ResponseUnmarshaller unmarshaller = new ResponseUnmarshaller(store, xmlr);
unmarshaller.skip();
Album album = unmarshaller.unmarshallAlbum(new Date());
System.currentTimeMillis();
}
public static void loadTracks() throws Exception {
Store store = new MemoryStore();
XMLStreamReader xmlr = createReader(new InputStreamReader(new FileInputStream(new java.io.File("src/main/resources/response/xml/load_tracks_1245076237936.xml")), "UTF8"));
ResponseUnmarshaller unmarshaller = new ResponseUnmarshaller(store, xmlr);
unmarshaller.skip();
List<Track> tracks = unmarshaller.unmarshallLoadTracks();
System.currentTimeMillis();
}
public static XMLStreamReader createReader(Reader xml) throws XMLStreamException {
XMLInputFactory xmlif = XMLInputFactory.newInstance();
xmlif.setEventAllocator(new XMLEventAllocatorImpl());
return xmlif.createXMLStreamReader(xml);
}
private Store store;
private XMLStreamReader xmlr;
public ResponseUnmarshaller(Store store, XMLStreamReader xmlr) {
this.store = store;
this.xmlr = xmlr;
}
public Result unmarshallSearchResult() throws XMLStreamException {
Date now = new Date();
Result result = new Result();
int eventType;
String localName;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("version".equals(localName)) {
skip();
} else if ("total-tracks".equals(localName)) {
result.setTotalTracks(getInteger());
} else if ("total-albums".equals(localName)) {
result.setTotalAlbums(getInteger());
} else if ("total-artists".equals(localName)) {
result.setTotalArtists(getInteger());
} else if ("tracks".equals(localName)) {
result.setTracks(new ArrayList<Track>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("track".equals(localName)) {
result.getTracks().add(unmarshallTrack(now));
} else {
throw unexpected();
}
}
} else if ("albums".equals(localName)) {
result.setAlbums(new ArrayList<Album>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("album".equals(localName)) {
result.getAlbums().add(unmarshallAlbum(null));
} else {
throw unexpected();
}
}
} else if ("artists".equals(localName)) {
result.setArtists(new ArrayList<Artist>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("artist".equals(localName)) {
result.getArtists().add(unmarshallArtist(null));
} else {
throw unexpected();
}
}
} else {
throw unexpected();
}
}
return result;
}
public List<Track> unmarshallLoadTracks() throws XMLStreamException {
Date now = new Date();
List<Track> tracks = new ArrayList<Track>();
int eventType;
String localName;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("version".equals(localName)) {
skip();
} else if ("total-tracks".equals(localName)) {
skip();
} else if ("tracks".equals(localName)) {
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("track".equals(localName)) {
tracks.add(unmarshallTrack(now));
} else {
throw unexpected();
}
}
} else {
throw unexpected();
}
}
return tracks;
}
/**
* <xs:complexType name="restrictionsType">
* <xs:sequence>
* <xs:element type="restrictionType" name="restriction"/>
* </xs:sequence>
* </xs:complexType>
* <p/>
* <xs:complexType name="restrictionType">
* <xs:simpleContent>
* <xs:extension base="xs:string">
* <xs:attribute type="xs:string" name="allowed" use="optional"/>
* <xs:attribute type="xs:string" name="catalogues" use="optional"/>
* <xs:attribute type="xs:string" name="forbidden" use="optional"/>
* </xs:extension>
* </xs:simpleContent>
* </xs:complexType>
*
* @param restrictedMedia
*/
public boolean unmarshalRestrictedMedia(RestrictedMedia restrictedMedia) throws XMLStreamException {
String localName = xmlr.getLocalName();
if ("restrictions".equals(localName)) {
restrictedMedia.setRestrictions(new ArrayList<Restriction>());
int eventType;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("restriction".equals(localName)) {
Restriction restriction = new Restriction();
for (int i = 0; i < xmlr.getAttributeCount(); i++) {
String attribute = xmlr.getAttributeName(i).getLocalPart();
String value = xmlr.getAttributeValue(i).trim();
if (!"".equals(value)) {
List<String> values = Arrays.asList(value.split(",|\\s+"));
if ("allowed".equals(attribute)) {
restriction.setAllowed(new HashSet<String>(values));
} else if ("catalogues".equals(attribute)) {
restriction.setCatalogues(new HashSet<String>(values));
} else if ("forbidden".equals(attribute)) {
restriction.setForbidden(new HashSet<String>(values));
} else {
throw unexpected();
}
}
}
skip();
restrictedMedia.getRestrictions().add(restriction);
} else {
throw unexpected();
}
}
} else if ("allowed".equals(localName)) {
skip();
} else if ("forbidden".equals(localName)) {
skip();
} else {
return false;
}
return true;
}
public Track unmarshallTrack(Date fullyLoaded) throws XMLStreamException {
Track track = null;
List<Track> redirects = null;
// sometimes album name occurs prior to the album id.
String albumName = null;
String albumArtistName = null;
int eventType;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
String localName = xmlr.getLocalName();
if ("id".equals(localName)) {
track = store.getTrack(xmlr.getElementText());
} else if ("title".equals(localName)) {
track.setTitle(getString());
} else if ("artist-id".equals(localName)) {
track.setArtist(store.getArtist(getString()));
} else if ("artist".equals(localName)) {
track.getArtist().setName(getString());
} else if ("album-artist".equals(localName)) {
if (track.getAlbum().getMainArtist() == null) {
albumArtistName = getString();
} else {
track.getAlbum().getMainArtist().setName(getString());
}
} else if ("album-artist-id".equals(localName)) {
track.getAlbum().setMainArtist((store.getArtist(getString())));
if (albumArtistName != null) {
track.getAlbum().getMainArtist().setName(albumArtistName);
albumArtistName = null;
}
} else if ("album".equals(localName)) {
if (track.getAlbum() == null) {
albumName = getString();
} else {
track.getAlbum().setName(xmlr.getElementText());
}
} else if ("album-id".equals(localName)) {
track.setAlbum(store.getAlbum(getString()));
if (albumName != null) {
track.getAlbum().setName(albumName);
albumName = null;
}
} else if ("year".equals(localName)) {
track.setYear(getInteger());
} else if ("track-number".equals(localName)) {
track.setTrackNumber(getInteger());
} else if ("length".equals(localName)) {
track.setLength(getInteger());
} else if ("redirect".equals(localName)) {
if (redirects == null) {
redirects = new ArrayList<Track>();
}
redirects.add(store.getTrack(getString()));
} else if ("files".equals(localName)) {
track.setFiles(new ArrayList<File>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("file".equals(localName)) {
File file = null;
for (int i = 0; i < xmlr.getAttributeCount(); i++) {
String attribute = xmlr.getAttributeName(i).getLocalPart();
if ("id".equals(attribute)) {
file = store.getFile(xmlr.getAttributeValue(i));
} else if ("format".equals(attribute)) {
file.setFormat(xmlr.getAttributeValue(i));
} else {
throw unexpected();
}
}
track.getFiles().add(file);
skip();
} else {
throw unexpected();
}
}
} else if ("links".equals(localName)) {
skipLinks();
} else if ("album-links".equals(localName)) {
skipLinks();
} else if ("cover".equals(localName)) {
track.setCover(store.getImage(getString()));
} else if ("popularity".equals(localName)) {
track.setPopularity(getFloat());
} else if ("similar-tracks".equals(localName)) {
track.setSimilarTracks(new ArrayList<Track>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("id".equals(localName)) {
track.getSimilarTracks().add(store.getTrack(getString()));
} else {
throw unexpected();
}
}
} else if ("external-ids".equals(localName)) {
track.setExternalIds(unmarshallExternalIds());
} else if (!unmarshalRestrictedMedia(track)) {
throw unexpected();
}
}
// we replace all redirects if they were loaded.
if (redirects != null) {
track.setRedirections(redirects);
}
if (fullyLoaded != null) {
track.setLoaded(fullyLoaded);
}
return track;
}
private List<ExternalId> unmarshallExternalIds() throws XMLStreamException {
int eventType;
String localName;
List<ExternalId> externalIds = new ArrayList<ExternalId>();
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("external-id".equals(localName)) {
ExternalId externalId = new ExternalId();
for (int i = 0; i < xmlr.getAttributeCount(); i++) {
String attribute = xmlr.getAttributeName(i).getLocalPart();
if ("type".equals(attribute)) {
externalId.setType(xmlr.getAttributeValue(i));
} else if ("id".equals(attribute)) {
externalId.setExternalId(xmlr.getAttributeValue(i));
} else {
throw unexpected();
}
}
externalIds.add(externalId);
skip();
} else {
throw unexpected();
}
}
return externalIds;
}
private void skipLinks() throws XMLStreamException {
int eventType;
String localName;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("link".equals(localName)) {
eventType = skip(); // end element
// todo keep these? i think its buy-album buttons or something like that
} else {
throw unexpected();
}
}
}
public Album unmarshallAlbum(Date fullyLoaded) throws XMLStreamException {
Album album = null;
String albumName = null; // sometimes album name occurs prior to the album id.
String artistName = null; // sometimes artist name occurs prior to the artist id.
int eventType;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
String localName = xmlr.getLocalName();
if ("id".equals(localName)) {
album = store.getAlbum(xmlr.getElementText());
if (albumName != null) {
album.setName(albumName);
albumName = null;
}
} else if ("version".equals(localName)) {
skip();
} else if ("name".equals(localName)) {
if (album == null) {
albumName = xmlr.getElementText();
} else {
album.setName(xmlr.getElementText());
}
} else if ("artist-id".equals(localName)) {
album.setMainArtist(store.getArtist(xmlr.getElementText()));
if (artistName != null) {
album.getMainArtist().setName(artistName);
artistName = null;
}
} else if ("artist".equals(localName) || "artist-name".equals(localName)) {
if (album == null || album.getMainArtist() == null) {
artistName = xmlr.getElementText();
} else {
album.getMainArtist().setName(xmlr.getElementText());
}
} else if ("album-type".equals(localName)) {
album.setType(xmlr.getElementText());
} else if ("popularity".equals(localName)) {
album.setPopularity(getFloat());
} else if ("year".equals(localName)) {
album.setYear(getInteger());
} else if ("cover".equals(localName)) {
album.setCover(store.getImage(xmlr.getElementText()));
} else if ("copyright".equals(localName)) {
album.setP(new ArrayList<String>());
album.setC(new ArrayList<String>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("c".equals(localName)) {
album.getC().add(xmlr.getElementText());
} else if ("p".equals(localName)) {
album.getP().add(xmlr.getElementText());
} else {
throw unexpected();
}
}
} else if ("review".equals(localName)) {
album.setReview(xmlr.getElementText());
} else if ("links".equals(localName)) {
skipLinks();
} else if ("discs".equals(localName)) {
album.setTracks(new ArrayList<Track>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("disc".equals(localName)) {
Integer discNumber = null;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("disc-number".equals(localName)) {
discNumber = Integer.valueOf(xmlr.getElementText());
} else if ("name".equals(localName)) {
album.setDiscName(discNumber - 1, xmlr.getElementText());
} else if ("track".equals(localName)) {
Track track = unmarshallTrack(fullyLoaded);
track.setDiscNumber(discNumber);
track.setAlbum(album);
album.getTracks().add(track);
} else {
throw unexpected();
}
}
} else {
throw unexpected();
}
}
} else if ("similar-albums".equals(localName)) {
album.setSimilarAlbums(new ArrayList<Album>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("id".equals(localName)) {
album.getSimilarAlbums().add(store.getAlbum(xmlr.getElementText()));
} else {
throw unexpected();
}
}
} else if ("external-ids".equals(localName)) {
album.setExternalIds(unmarshallExternalIds());
} else if (!unmarshalRestrictedMedia(album)) {
throw unexpected();
}
}
if (fullyLoaded != null) {
album.setLoaded(fullyLoaded);
}
return album;
}
/**
* <xs:complexType name="artistType">
* <xs:choice maxOccurs="unbounded" minOccurs="0">
* <xs:element type="hex_32" name="id"/>
* <xs:element type="xs:string" name="name"/>
* <xs:element type="imageType" name="portrait" />
* <xs:element type="comma_seperated" name="genres"/>
* <xs:element type="xs:string" name="years-active"/>
* <xs:element type="xs:long" name="version"/>
* <xs:element type="biosType" name="bios"/>
* <xs:element type="similar-artistsType" name="similar-artists"/>
* <xs:element type="albumsType" name="albums"/>
* </xs:choice>
* </xs:complexType>
*
* @return
* @throws XMLStreamException
*/
public Artist unmarshallArtist(Date fullyLoaded) throws XMLStreamException {
Artist artist = null;
String artistName = null; // sometimes artist name occurs prior to the artist id.
int eventType;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
String localName = xmlr.getLocalName();
if ("id".equals(localName)) {
artist = store.getArtist(xmlr.getElementText());
if (artistName != null) {
artist.setName(artistName);
artistName = null;
}
} else if ("name".equals(localName)) {
if (artist == null) {
artistName = xmlr.getElementText();
} else {
artist.setName(xmlr.getElementText());
}
} else if ("popularity".equals(localName)) {
artist.setPopularity(getFloat());
} else if ("portrait".equals(localName)) {
artist.setPortrait(unmarshallImage());
} else if ("genres".equals(localName)) {
String string = xmlr.getElementText().trim();
if (!"".equals(string)) {
artist.setGenres(new HashSet<String>(Arrays.asList(string.split(","))));
}
} else if ("years-active".equals(localName)) {
artist.setYearsActive(new ArrayList<String>(Arrays.asList(xmlr.getElementText().split(","))));
} else if ("version".equals(localName)) {
// ignored, todo log something if not 1?
eventType = skip(); // end element
} else if ("bios".equals(localName)) {
if (artist.getBiographies() == null) {
artist.setBiographies(new ArrayList<Biography>());
}
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("bio".equals(localName)) {
Biography biography = new Biography();
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("text".equals(localName)) {
biography.setText(xmlr.getElementText());
} else if ("portraits".equals(localName)) {
biography.setPortraits(new ArrayList<Image>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("portrait".equals(localName)) {
biography.getPortraits().add(unmarshallImage());
} else {
throw unexpected();
}
}
} else {
throw unexpected();
}
}
artist.getBiographies().add(biography);
} else {
throw unexpected();
}
}
} else if ("similar-artists".equals(localName)) {
artist.setSimilarArtists(new ArrayList<Artist>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("artist".equals(localName)) {
artist.getSimilarArtists().add(unmarshallArtist(null));
} else {
throw unexpected();
}
}
} else if ("albums".equals(localName)) {
artist.setMainArtistAlbums(new ArrayList<Album>());
artist.setAllAlbumsWithTrackPresent(new ArrayList<Album>());
artist.setTracks(new ArrayList<Track>());
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
localName = xmlr.getLocalName();
if ("album".equals(localName)) {
Album album = unmarshallAlbum(fullyLoaded);
artist.getAllAlbumsWithTrackPresent().add(album);
if (artist.equals(album.getMainArtist())) {
artist.getMainArtistAlbums().add(album);
}
for (Track track : album.getTracks()) {
if (artist.equals(track.getArtist())) {
artist.getTracks().add(track);
}
}
} else {
throw unexpected();
}
}
} else if ("external-ids".equals(localName)) {
artist.setExternalIds(unmarshallExternalIds());
} else if (!unmarshalRestrictedMedia(artist)) {
throw unexpected();
}
}
if (fullyLoaded != null) {
artist.setLoaded(fullyLoaded);
}
return artist;
}
/**
* <xs:complexType name="imageType">
* <xs:sequence>
* <xs:element type="hex_40" name="id" minOccurs="0"/>
* <xs:element name="width" minOccurs="0" type="xs:short"/>
* <xs:element type="xs:short" name="height" minOccurs="0"/>
* </xs:sequence>
* </xs:complexType>
*
* @return
* @throws XMLStreamException
*/
public Image unmarshallImage() throws XMLStreamException {
Image image = null;
int eventType;
while ((eventType = skip()) == XMLStreamConstants.START_ELEMENT) {
String localName = xmlr.getLocalName();
if ("id".equals(localName)) {
image = store.getImage(xmlr.getElementText());
} else if ("width".equals(localName)) {
image.setWidth(Integer.valueOf(xmlr.getElementText()));
} else if ("height".equals(localName)) {
image.setHeight(Integer.valueOf(xmlr.getElementText()));
} else {
throw unexpected();
}
}
return image;
}
public XMLStreamException unexpected() {
String tag = xmlr.getLocalName();
String value;
try {
value = xmlr.getElementText();
} catch (Exception e) {
value = "[error]";
}
StringBuilder sb = new StringBuilder();
sb.append("<").append(tag).append(">");
sb.append(value);
sb.append("</").append(tag).append(">");
return new XMLStreamException(sb.toString() + "\n" + xmlr.getLocation().toString());
}
public int skip() throws XMLStreamException {
int eventType = XMLEvent.COMMENT;
while (eventType != XMLEvent.START_ELEMENT && eventType != XMLEvent.END_ELEMENT) {
if (eventType == XMLEvent.ATTRIBUTE) {
System.currentTimeMillis();
}
eventType = xmlr.next();
}
return eventType;
}
public static String getEventTypeString(int eventType) {
switch (eventType) {
case XMLEvent.START_ELEMENT:
return "START_ELEMENT";
case XMLEvent.END_ELEMENT:
return "END_ELEMENT";
case XMLEvent.PROCESSING_INSTRUCTION:
return "PROCESSING_INSTRUCTION";
case XMLEvent.CHARACTERS:
return "CHARACTERS";
case XMLEvent.COMMENT:
return "COMMENT";
case XMLEvent.START_DOCUMENT:
return "START_DOCUMENT";
case XMLEvent.END_DOCUMENT:
return "END_DOCUMENT";
case XMLEvent.ENTITY_REFERENCE:
return "ENTITY_REFERENCE";
case XMLEvent.ATTRIBUTE:
return "ATTRIBUTE";
case XMLEvent.DTD:
return "DTD";
case XMLEvent.CDATA:
return "CDATA";
case XMLEvent.SPACE:
return "SPACE";
}
return "UNKNOWN_EVENT_TYPE , " + eventType;
}
public String getString() throws XMLStreamException {
String text = xmlr.getElementText();
if ("".equals(text)) {
return null;
}
return text;
}
public Float getFloat() throws XMLStreamException {
String text = xmlr.getElementText();
if ("".equals(text)) {
return null;
}
return Float.valueOf(text);
}
public Integer getInteger() throws XMLStreamException {
String text = xmlr.getElementText();
if ("".equals(text)) {
return null;
}
return Integer.valueOf(text);
}
}