package org.androiddaisyreader.model;
import static org.androiddaisyreader.model.XmlUtilities.obtainEncodingStringFromInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
public class Smil30Specification extends DefaultHandler {
private Element current;
private Part.Builder partBuilder;
private List<Part> parts = new ArrayList<Part>();
private BookContext context;
private boolean handlingPar = false;
private boolean isProdNote = false;
private String textProdNote = null;
private String idPrevious = null;
private String currentContentsFilename;
private Document doc;
private static final int DAISYFORMAT30 = 30;
/**
* Create an object representing a SMIL version 1.0 Specification.
*
* @param context the BookContext used to locate references to files in the
* SMIL file.
*/
private Smil30Specification(BookContext context) {
this.context = context;
}
/**
* Factory method that returns the Parts discovered in the contents.
*
* TODO 20120303 (jharty): review the exception handling as all exceptions
* are currently converted to RuntimeExceptions which makes them harder to
* interpret by calling code. Note: this rework should be across the entire
* body of code, not just for this method.
*
* @param context BookContext used to locate files that comprise the book
* @param contents The contents to parse to extract the Parts.
* @return The parts discovered in the contents.
*/
public static Part[] getParts(BookContext context, InputStream contents) {
Smil30Specification smil = new Smil30Specification(context);
try {
XMLReader saxParser = Smil.getSaxParser();
saxParser.setContentHandler(smil);
saxParser.parse(Smil.getInputSource(contents));
contents.close();
return smil.getParts();
} catch (SAXException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Get the Parts discovered in this SMIL contents.
*
* @return The Parts.
*/
private Part[] getParts() {
return parts.toArray(new Part[0]);
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
current = elementMap.get(ParserUtilities.getName(localName, name));
if (current == null) {
return;
}
switch (current) {
case PAR:
handlingPar = false;
addPartToSection();
break;
case SEQ:
if (isProdNote) {
isProdNote = false;
}
break;
case AUDIO:
case TEXT:
if (!handlingPar) {
addPartToSection();
}
default:
break;
}
}
private void addPartToSection() {
parts.add(partBuilder.build());
}
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) {
current = elementMap.get(ParserUtilities.getName(localName, name));
if (current == null) {
return;
}
switch (current) {
case AUDIO:
if (!handlingPar) {
newPart();
}
handleAudio(attributes);
break;
case META:
handleMeta(attributes);
break;
case PAR:
handlingPar = true;
handlePar(attributes);
break;
case SEQ:
// do nothing.
break;
case TEXT:
if (!handlingPar) {
newPart();
}
handleTextElement(attributes);
break;
default:
// Record the element(s) we don't handle in case we can improve our
// processing of smil files.
recordUnhandledElement(current, attributes);
break;
}
}
private void handlePar(Attributes attributes) {
newPart();
String id = ParserUtilities.getValueForName("id", attributes);
partBuilder.setId(id);
if (getClass(attributes) != null && getClass(attributes).equals("prodnote")) {
isProdNote = true;
}
}
private void newPart() {
partBuilder = new Part.Builder();
}
/**
* Handle the Text Element.
*
* The text element stores the location of a text fragment in an id
* attribute.
*
* @param attributes
*/
private void handleTextElement(Attributes attributes) {
String src = ParserUtilities.getValueForName("src", attributes);
// TODO 20120207 (jharty) Refactor for a text reference into a html file
// Create HTML Snippet Reader
String[] elements = DaisySnippet.parseCompositeReference(src);
String uri = elements[0];
String id = elements[1];
// We need to create the jsoup document if it's not initialised, or if
// the filename has changed (which means the contents are no longer
// valid.
if (doc == null || !uri.equalsIgnoreCase(currentContentsFilename)) {
try {
InputStream contents = context.getResource(uri);
String encoding = obtainEncodingStringFromInputStream(contents);
doc = Jsoup.parse(contents, encoding, context.getBaseUri());
currentContentsFilename = uri;
} catch (IOException ioe) {
// TODO 20120214 (jharty): we need to consider more appropriate
// error reporting.
throw new RuntimeException("TODO fix me", ioe);
}
}
if (!isProdNote) {
if (textProdNote != null) {
doc.getElementById(idPrevious).appendText(" " + textProdNote);
textProdNote = null;
}
idPrevious = id;
partBuilder.addSnippet(new DaisySnippet(doc, id));
} else {
textProdNote = doc.getElementById(id).text();
}
}
private void recordUnhandledElement(Element element, Attributes attributes) {
StringBuilder elementDetails = new StringBuilder();
elementDetails.append(String.format("[%s ", element.toString()));
for (int i = 0; i < attributes.getLength(); i++) {
elementDetails.append(String.format("%s=%s", attributes.getLocalName(i),
attributes.getValue(i)));
}
elementDetails.append("]");
}
private void handleAudio(Attributes attributes) {
String audioFilename = ParserUtilities.getValueForName("src", attributes);
int clipBegin = ExtractTimingValues.extractTimingAsMilliSeconds("clipBegin", attributes,
DAISYFORMAT30);
int clipEnd = ExtractTimingValues.extractTimingAsMilliSeconds("clipEnd", attributes,
DAISYFORMAT30);
String id = ParserUtilities.getValueForName("id", attributes);
Audio audio = new Audio(id, audioFilename, clipBegin, clipEnd);
partBuilder.addAudio(audio);
}
private void handleMeta(Attributes attributes) {
String metaName = Smil.handleMeta(attributes);
Meta meta = metaMap.get(metaName);
if (meta == null) {
return;
}
}
private String getClass(Attributes attributes) {
return ParserUtilities.getValueForName("class", attributes);
}
private enum Element {
AUDIO, META, PAR, SEQ, TEXT;
@Override
public String toString() {
return this.name().toLowerCase();
}
}
private static Map<String, Element> elementMap = new HashMap<String, Element>(
Element.values().length);
static {
for (Element e : Element.values()) {
elementMap.put(e.toString(), e);
}
}
private enum Meta {
FORMAT {
@Override
public String toString() {
return "dc:format";
}
}
// Add more enums as we need them.
}
private static Map<String, Meta> metaMap = new HashMap<String, Meta>(Meta.values().length);
static {
for (Meta m : Meta.values()) {
metaMap.put(m.toString(), m);
}
}
}