Skip to content

Commit

Permalink
Parse itunes plist file without JAXB (#11)
Browse files Browse the repository at this point in the history
Use built in parsing instead.
  • Loading branch information
bolsinga authored May 24, 2020
1 parent 11f9ea7 commit bf0402a
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 205 deletions.
31 changes: 1 addition & 30 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,40 +37,11 @@
<mkdir dir="${dst.lib}" />
</target>

<target name="plist_binding" depends="prepare">
<exec executable="xjc" >
<arg line="-p com.bolsinga.plist.data -d ${build.gensrc} ${basedir}/xml/PropertyList-1.0.xsd" />
</exec>
<javac destdir="${build.classes}"
debug="${javac.debug}"
deprecation="on"
includeantruntime="false">
<src path="${build.gensrc}" />
<include name="com/bolsinga/plist/data/**" />
</javac>
<copy todir="${build.classes}">
<fileset dir="${build.gensrc}" >
<exclude name="**/*.java" />
</fileset>
</copy>
</target>

<path id="web_classpath">
<pathelement path="${lib.dir}/ecs-1.4.2.jar" />
<pathelement path="${lib.dir}/json.jar" />
</path>

<target name="plist_utilities" depends="plist_binding">
<javac destdir="${build.classes}"
debug="${javac.debug}"
deprecation="on"
includeantruntime="false">
<compilerarg value="${javac.lint}" />
<src path="${src.dir}" />
<include name="com/bolsinga/plist/*.java" />
</javac>
</target>

<target name="web">
<javac destdir="${build.classes}"
debug="${javac.debug}"
Expand All @@ -97,7 +68,7 @@
</copy>
</target>

<target name="itunes" depends="plist_utilities">
<target name="itunes" depends="prepare">
<javac destdir="${build.classes}"
debug="${javac.debug}"
deprecation="on"
Expand Down
151 changes: 50 additions & 101 deletions src/com/bolsinga/itunes/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
import java.io.*;
import java.util.*;
import java.util.regex.*;

import javax.xml.bind.*;
import javax.xml.datatype.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

public class Parser {
private static final String TK_GENRE_VOICE_MEMO = "Voice Memo";
Expand Down Expand Up @@ -82,7 +81,8 @@ private void addTrack(final Track track) {
int year = (track.getYear() != null) ? Integer.parseInt(track.getYear()) : 0;
int trackNumber = (track.getTrack_Number() != null) ? Integer.parseInt(track.getTrack_Number()) : 0;
int playCount = (track.getPlay_Count() != null) ? Integer.parseInt(track.getPlay_Count()) : 0;
GregorianCalendar lastPlayed = (track.getPlay_Date_UTC() != null) ? com.bolsinga.web.Util.fromJSONCalendar(track.getPlay_Date_UTC()) : null;
java.time.ZonedDateTime lastPlayedZDT = (track.getPlay_Date_UTC() != null) ? java.time.ZonedDateTime.parse(track.getPlay_Date_UTC()) : null;
GregorianCalendar lastPlayed = (lastPlayedZDT != null) ? GregorianCalendar.from(lastPlayedZDT): null;
createTrack(track.getArtist(), track.getSort_Artist(), track.getName(), track.getAlbum(), year, trackNumber, track.getGenre(), lastPlayed, playCount, compilation);
}
}
Expand Down Expand Up @@ -203,123 +203,72 @@ private void setAlbumYears() {
}
}

private static com.bolsinga.plist.data.Plist createPlist(final String sourceFile) throws ParserException {
com.bolsinga.plist.data.Plist plist = null;

private static Map<String, Object> createPlist(final String sourceFile) throws ParserException {
InputStream is = null;
try {
try {
is = new FileInputStream(sourceFile);
} catch (FileNotFoundException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't find plist file: ");
sb.append(sourceFile);
throw new ParserException(sb.toString(), e);
}

javax.xml.stream.XMLStreamReader xmlStreamReader = null;
try {
javax.xml.stream.XMLInputFactory xmlInputFactory = javax.xml.stream.XMLInputFactory.newInstance();
xmlInputFactory.setProperty(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, "http");
xmlStreamReader = xmlInputFactory.createXMLStreamReader(is);
} catch (javax.xml.stream.XMLStreamException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't create XML Reader: ");
sb.append(is);
throw new ParserException(sb.toString(), e);
}

try {
JAXBContext jc = JAXBContext.newInstance("com.bolsinga.plist.data");
Unmarshaller u = jc.createUnmarshaller();

plist = (com.bolsinga.plist.data.Plist)u.unmarshal(xmlStreamReader);
} catch (JAXBException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't unmarshal plist file: ");
sb.append(sourceFile);
throw new ParserException(sb.toString(), e);
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
StringBuilder sb = new StringBuilder();
sb.append("Unable to close plist file: ");
sb.append(sourceFile);
throw new ParserException(sb.toString(), e);
}
}
is = new FileInputStream(sourceFile);
} catch (FileNotFoundException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't find plist file: ");
sb.append(sourceFile);
throw new ParserException(sb.toString(), e);
}

return plist;
}

private static List<Track> createTracks(final String sourceFile) throws ParserException {
com.bolsinga.plist.data.Dict dict = createTracksDict(sourceFile);

ArrayList<Track> tracks = new ArrayList<Track>();
XMLReader parser = null;
try {
parser = XMLReaderFactory.createXMLReader();
} catch (SAXException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't create XMLReader");
throw new ParserException(sb.toString(), e);
}

Iterator<Object> i = dict.getKeyAndArrayOrData().iterator();
while (i.hasNext()) {
Object key = i.next(); // key not used
ParserHandler handler = new ParserHandler();
parser.setContentHandler(handler);

com.bolsinga.plist.data.Dict trackDict = (com.bolsinga.plist.data.Dict)i.next();
Track track = createTrack(trackDict);
tracks.add(track);
InputSource source = new InputSource(is);
try {
parser.parse(source);
} catch (IOException | SAXException e) {
StringBuilder sb = new StringBuilder();
sb.append("Can't parse InputSource");
throw new ParserException(sb.toString(), e);
}

return tracks;
return handler.plist;
}

private static com.bolsinga.plist.data.Dict createTracksDict(final String sourceFile) throws ParserException {
com.bolsinga.plist.data.Plist plist = createPlist(sourceFile);
private static Map<String, Object> createTracksDict(final String sourceFile) throws ParserException {
Map<String, Object> plist = createPlist(sourceFile);

Iterator<Object> i = plist.getDict().getKeyAndArrayOrData().iterator();
while (i.hasNext()) {
JAXBElement<? extends Object> jo = (JAXBElement<? extends Object>)i.next();
String key = (String)jo.getValue();
if (key.equals("Tracks")) {
com.bolsinga.plist.data.Dict dict = (com.bolsinga.plist.data.Dict)i.next();
for (Map.Entry<String, Object> entry : plist.entrySet()) {
if (entry.getKey().equals("Tracks")) {
@SuppressWarnings("unchecked")
Map<String, Object> dict = (Map<String, Object>)entry.getValue();
return dict;
} else {
Object o = i.next();
}
}
throw new ParserException("No Tracks key in plist: " + plist.toString());
}

private static Track createTrack(final com.bolsinga.plist.data.Dict trackDict) throws ParserException {
Iterator<Object> i = trackDict.getKeyAndArrayOrData().iterator();

Track t = new Track();

while (i.hasNext()) {
JAXBElement<? extends Object> jokey = (JAXBElement<? extends Object>)i.next();
String key = (String)jokey.getValue();

// always pull off the value, it may be unused.
JAXBElement<? extends Object> jovalue = (JAXBElement<? extends Object>)i.next();

Object value = jovalue.getValue();
String stringValue = null;
if (value instanceof java.lang.String) {
stringValue = (String)value;
} else if (value instanceof java.lang.Number) {
stringValue = String.valueOf((Number)value);
} else if (value instanceof javax.xml.datatype.XMLGregorianCalendar) {
GregorianCalendar gc = ((XMLGregorianCalendar)value).toGregorianCalendar();
stringValue = com.bolsinga.web.Util.toJSONCalendar(gc);
} else if (value instanceof java.lang.Object && ("true".equals(jovalue.getName().getLocalPart()) || "false".equals(jovalue.getName().getLocalPart()))) {
stringValue = jovalue.getName().getLocalPart();
} else {
throw new ParserException("Unhandled JAXB Name: " + jovalue.getName() + " Value: " + value.toString() + " Type: " + jovalue.getDeclaredType().toString());
private static List<Track> createTracks(final String sourceFile) throws ParserException {
Map<String, Object> tracksDict = createTracksDict(sourceFile);

ArrayList<Track> tracks = new ArrayList<Track>();

for (Object o : tracksDict.values()) {
@SuppressWarnings("unchecked")
Map<String, Object> trackDict = (Map<String, Object>)o;

Track track = new Track();

for (Map.Entry<String, Object> entry : trackDict.entrySet()) {
track.set(entry.getKey(), (String)entry.getValue());
}

t.set(key, stringValue);
tracks.add(track);
}

return t;
return tracks;
}
}
134 changes: 134 additions & 0 deletions src/com/bolsinga/itunes/ParserHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.bolsinga.itunes;

import java.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

class ParserHandler extends DefaultHandler {
public Map<String, Object> plist;

private Stack<AbstractMap.SimpleEntry<String, Object>> mapEntryStack = new Stack<AbstractMap.SimpleEntry<String, Object>>();

private Stack<String> elementStack = new Stack<String>();

private Stack<Map<String, Object>> dictStack = new Stack<Map<String, Object>>();

private Stack<Boolean> parsingArrayStack = new Stack<Boolean>();

public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
this.elementStack.push(qName);

if ("array".equals(qName)) {
if (this.parsingArrayStack.empty()) {
this.mapEntryStack.pop(); // Remove the key for this array.
}
this.parsingArrayStack.push(Boolean.TRUE);
} else {
if (this.parsingArrayStack.empty()) {
if ("plist".equals(qName)) {
// Nothing
} else if ("dict".equals(qName)) {
Map<String, Object> dictionary = new LinkedHashMap<String, Object>();
this.dictStack.push(dictionary);
} else if ("key".equals(qName)) {
// Nothing
} else {
if ("data".equals(qName) ||
"date".equals(qName) ||
"false".equals(qName) ||
"integer".equals(qName) ||
"string".equals(qName) ||
"true".equals(qName)) {
// Nothing
} else {
throw new SAXException("Unknown element: " + qName);
}
}
}
}
}

public void endElement(String uri, String localName, String qName) throws SAXException {
this.elementStack.pop();

if ("array".equals(qName)) {
this.parsingArrayStack.pop();
} else {
if (this.parsingArrayStack.empty()) {
if ("plist".equals(qName)) {
plist = dictStack.pop();
} else if ("dict".equals(qName)) {
AbstractMap.SimpleEntry<String, Object> currentMapEntry = currentMapEntry();
if (currentMapEntry != null) {
if (currentMapEntry.getValue() != null) {
throw new SAXException("Can't set dictionary value for: " + currentMapEntry.toString());
}
Map<String, Object> endedDict = dictStack.pop();
currentMapEntry.setValue(endedDict);

// Now pop it
currentMapEntry = this.mapEntryStack.pop();

// need to re-access the new current dict
currentDict().put(currentMapEntry.getKey(), currentMapEntry.getValue());
} else {
// A-OK
}
} else if ("key".equals(qName)) {
// Nothing
} else {
if ("data".equals(qName) ||
"date".equals(qName) ||
"integer".equals(qName) ||
"string".equals(qName)) {
AbstractMap.SimpleEntry<String, Object> mapEntry = this.mapEntryStack.pop();
currentDict().put(mapEntry.getKey(), mapEntry.getValue());
} else if ("true".equals(qName) || "false".equals(qName)) {
AbstractMap.SimpleEntry<String, Object> mapEntry = this.mapEntryStack.pop();
mapEntry.setValue(qName);
currentDict().put(mapEntry.getKey(), mapEntry.getValue());
} else {
throw new SAXException("Unknown element: " + qName);
}
}
}
}
}

public void characters(char[] ch, int start, int length) throws SAXException {
String value = new String(ch, start, length);

if (this.parsingArrayStack.empty()) {
if ("key".equals(elementStack.peek())) {
AbstractMap.SimpleEntry<String, Object> mapEntry = new AbstractMap.SimpleEntry<String, Object>(value, null);
this.mapEntryStack.push(mapEntry);
} else {
AbstractMap.SimpleEntry<String, Object> currentMapEntry = currentMapEntry();
String currentValue = (String)currentMapEntry.getValue();
if (currentValue != null) {
StringBuffer sb = new StringBuffer(currentValue);
sb.append(value);
currentMapEntry.setValue(sb.toString());
} else {
currentMapEntry.setValue(value);
}
}
}
}

private Map<String, Object> currentDict() {
if (this.dictStack.empty()) {
return null;
} else {
return this.dictStack.peek();
}
}

private AbstractMap.SimpleEntry<String, Object> currentMapEntry() {
if (this.mapEntryStack.empty()) {
return null;
} else {
return this.mapEntryStack.peek();
}
}
}
Loading

0 comments on commit bf0402a

Please sign in to comment.