/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.support.contentdirectory;

import org.teleal.cling.model.types.InvalidValueException;
import org.teleal.cling.support.model.ProtocolInfo;
import org.teleal.cling.support.model.DIDLContent;
import org.teleal.cling.support.model.DIDLObject;
import org.teleal.cling.support.model.DescMeta;
import org.teleal.cling.support.model.Person;
import org.teleal.cling.support.model.PersonWithRole;
import org.teleal.cling.support.model.Res;
import org.teleal.cling.support.model.StorageMedium;
import org.teleal.cling.support.model.WriteStatus;
import org.teleal.cling.support.model.container.Container;
import org.teleal.cling.support.model.item.Item;
import org.teleal.common.io.IO;
import org.teleal.common.xml.SAXParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.teleal.cling.model.XMLUtil.appendNewElement;
import static org.teleal.cling.model.XMLUtil.appendNewElementIfNotNull;

/**
 * DIDL parser based on SAX for reading and DOM for writing.
 * <p>
 * This parser requires Android platform level 8 (2.2).
 * </p>
 * <p>
 * Override the {@link #createDescMetaHandler(org.teleal.cling.support.model.DescMeta, org.teleal.common.xml.SAXParser.Handler)}
 * method to read vendor extension content of {@code <desc>} elements. You then should also override the
 * {@link #populateDescMetadata(org.w3c.dom.Element, org.teleal.cling.support.model.DescMeta)} method for writing.
 * </p>
 * <p>
 * Override the {@link #createItemHandler(org.teleal.cling.support.model.item.Item, org.teleal.common.xml.SAXParser.Handler)}
 * etc. methods to register custom handlers for vendor-specific elements and attributes within items, containers,
 * and so on.
 * </p>
 *
 * @author Christian Bauer
 */
public class DIDLParser extends SAXParser {

    final private static Logger log = Logger.getLogger(DIDLParser.class.getName());

    /**
     * Uses the current thread's context classloader to read and unmarshall the given resource.
     *
     * @param resource The resource on the classpath.
     * @return The unmarshalled DIDL content model.
     * @throws Exception
     */
    public DIDLContent parseResource(String resource) throws Exception {
        InputStream is = null;
        try {
            is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
            return parse(IO.readLines(is));
        } finally {
            if (is != null) is.close();
        }
    }

    /**
     * Reads and unmarshalls an XML representation into a DIDL content model.
     *
     * @param xml The XML representation.
     * @return A DIDL content model.
     * @throws Exception
     */
    public DIDLContent parse(String xml) throws Exception {

        if (xml == null || xml.length() == 0) {
            throw new RuntimeException("Null or empty XML");
        }

        DIDLContent content = new DIDLContent();
        createRootHandler(content, this);

        log.fine("Parsing DIDL XML content");
        parse(new InputSource(new StringReader(xml)));
        return content;
    }

    protected RootHandler createRootHandler(DIDLContent instance, SAXParser parser) {
        return new RootHandler(instance, parser);
    }

    protected ContainerHandler createContainerHandler(Container instance, Handler parent) {
        return new ContainerHandler(instance, parent);
    }

    protected ItemHandler createItemHandler(Item instance, Handler parent) {
        return new ItemHandler(instance, parent);
    }

    protected ResHandler createResHandler(Res instance, Handler parent) {
        return new ResHandler(instance, parent);
    }

    protected DescMetaHandler createDescMetaHandler(DescMeta instance, Handler parent) {
        return new DescMetaHandler(instance, parent);
    }


    protected Container createContainer(Attributes attributes) {
        Container container = new Container();

        container.setId(attributes.getValue("id"));
        container.setParentID(attributes.getValue("parentID"));

        if ((attributes.getValue("childCount") != null))
            container.setChildCount(Integer.valueOf(attributes.getValue("childCount")));

        if ((attributes.getValue("restricted") != null))
            container.setRestricted(Boolean.valueOf(attributes.getValue("restricted")));

        if ((attributes.getValue("searchable") != null))
            container.setSearchable(Boolean.valueOf(attributes.getValue("searchable")));

        return container;
    }

    protected Item createItem(Attributes attributes) {
        Item item = new Item();

        item.setId(attributes.getValue("id"));
        item.setParentID(attributes.getValue("parentID"));

        if ((attributes.getValue("restricted") != null))
            item.setRestricted(Boolean.valueOf(attributes.getValue("restricted")));

        if ((attributes.getValue("refID") != null))
            item.setRefID(attributes.getValue("refID"));

        return item;
    }

    protected Res createResource(Attributes attributes) {
        Res res = new Res();

        if (attributes.getValue("importUri") != null)
            res.setImportUri(URI.create(attributes.getValue("importUri")));

        try {
            res.setProtocolInfo(
                    new ProtocolInfo(attributes.getValue("protocolInfo"))
            );
        } catch (InvalidValueException ex) {
            throw new RuntimeException("Invalid protocol info: " + ex, ex);
        }

        if (attributes.getValue("size") != null)
            res.setSize(Long.valueOf(attributes.getValue("size")));

        if (attributes.getValue("duration") != null)
            res.setDuration(attributes.getValue("duration"));

        if (attributes.getValue("bitrate") != null)
            res.setBitrate(Long.valueOf(attributes.getValue("bitrate")));

        if (attributes.getValue("sampleFrequency") != null)
            res.setSampleFrequency(Long.valueOf(attributes.getValue("sampleFrequency")));

        if (attributes.getValue("bitsPerSample") != null)
            res.setBitsPerSample(Long.valueOf(attributes.getValue("bitsPerSample")));

        if (attributes.getValue("nrAudioChannels") != null)
            res.setNrAudioChannels(Long.valueOf(attributes.getValue("nrAudioChannels")));

        if (attributes.getValue("colorDepth") != null)
            res.setColorDepth(Long.valueOf(attributes.getValue("colorDepth")));

        if (attributes.getValue("protection") != null)
            res.setProtection(attributes.getValue("protection"));

        if (attributes.getValue("resolution") != null)
            res.setResolution(attributes.getValue("resolution"));

        return res;
    }

    protected DescMeta createDescMeta(Attributes attributes) {
        DescMeta desc = new DescMeta();

        desc.setId(attributes.getValue("id"));

        if ((attributes.getValue("type") != null))
            desc.setType(attributes.getValue("type"));

        if ((attributes.getValue("nameSpace") != null))
            desc.setNameSpace(URI.create(attributes.getValue("nameSpace")));

        return desc;
    }


    /* ############################################################################################# */


    /**
     * Generates a XML representation of the content model.
     * <p>
     * Items inside a container will <em>not</em> be represented in the XML, the containers
     * will be rendered flat without children.
     * </p>
     *
     * @param content The content model.
     * @return An XML representation.
     * @throws Exception
     */
    public String generate(DIDLContent content) throws Exception {
        return generate(content, false);
    }

    /**
     * Generates an XML representation of the content model.
     * <p>
     * Optionally, items inside a container will be represented in the XML,
     * the container elements then have nested item elements. Although this
     * parser can read such a structure, it is unclear whether other DIDL
     * parsers should and actually do support this XML.
     * </p>
     *
     * @param content     The content model.
     * @param nestedItems <code>true</code> if nested item elements should be rendered for containers.
     * @return An XML representation.
     * @throws Exception
     */
    public String generate(DIDLContent content, boolean nestedItems) throws Exception {
        return documentToString(buildDOM(content, nestedItems), true);
    }

    // TODO: Yes, this only runs on Android 2.2

    protected String documentToString(Document document, boolean omitProlog) throws Exception {
        TransformerFactory transFactory = TransformerFactory.newInstance();

        // Indentation not supported on Android 2.2
        //transFactory.setAttribute("indent-number", 4);

        Transformer transformer = transFactory.newTransformer();

        if (omitProlog) {
            // TODO: UPNP VIOLATION: Terratec Noxon Webradio fails when DIDL content has a prolog
            // No XML prolog! This is allowed because it is UTF-8 encoded and required
            // because broken devices will stumble on SOAP messages that contain (even
            // encoded) XML prologs within a message body.
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        }

        // Again, Android 2.2 fails hard if you try this.
        //transformer.setOutputProperty(OutputKeys.INDENT, "yes");

        StringWriter out = new StringWriter();
        transformer.transform(new DOMSource(document), new StreamResult(out));
        return out.toString();
    }

    protected Document buildDOM(DIDLContent content, boolean nestedItems) throws Exception {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        Document d = factory.newDocumentBuilder().newDocument();

        generateRoot(content, d, nestedItems);

        return d;
    }

    protected void generateRoot(DIDLContent content, Document descriptor, boolean nestedItems) {
        Element rootElement = descriptor.createElementNS(DIDLContent.NAMESPACE_URI, "DIDL-Lite");
        descriptor.appendChild(rootElement);

        // rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:didl", DIDLContent.NAMESPACE_URI);
        rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:upnp", DIDLObject.Property.UPNP.NAMESPACE.URI);
        rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dc", DIDLObject.Property.DC.NAMESPACE.URI);

        for (Container container : content.getContainers()) {
            if (container == null) continue;
            generateContainer(container, descriptor, rootElement, nestedItems);
        }

        for (Item item : content.getItems()) {
            if (item == null) continue;
            generateItem(item, descriptor, rootElement);
        }

        for (DescMeta descMeta : content.getDescMetadata()) {
            if (descMeta == null) continue;
            generateDescMetadata(descMeta, descriptor, rootElement);
        }
    }

    protected void generateContainer(Container container, Document descriptor, Element parent, boolean nestedItems) {

        if (container.getTitle() == null) {
            throw new RuntimeException("Missing 'dc:title' element for container: " + container.getId());
        }
        if (container.getClazz() == null) {
            throw new RuntimeException("Missing 'upnp:class' element for container: " + container.getId());
        }

        Element containerElement = appendNewElement(descriptor, parent, "container");

        if (container.getId() == null)
            throw new NullPointerException("Missing id on container: " + container);
        containerElement.setAttribute("id", container.getId());

        if (container.getParentID() == null)
            throw new NullPointerException("Missing parent id on container: " + container);
        containerElement.setAttribute("parentID", container.getParentID());

        if (container.getChildCount() != null) {
            containerElement.setAttribute("childCount", Integer.toString(container.getChildCount()));
        }

        containerElement.setAttribute("restricted", Boolean.toString(container.isRestricted()));
        containerElement.setAttribute("searchable", Boolean.toString(container.isSearchable()));

        appendNewElementIfNotNull(
                descriptor,
                containerElement,
                "dc:title",
                container.getTitle(),
                DIDLObject.Property.DC.NAMESPACE.URI
        );

        appendNewElementIfNotNull(
                descriptor,
                containerElement,
                "dc:creator",
                container.getCreator(),
                DIDLObject.Property.DC.NAMESPACE.URI
        );

        appendNewElementIfNotNull(
                descriptor,
                containerElement,
                "upnp:writeStatus",
                container.getWriteStatus(),
                DIDLObject.Property.UPNP.NAMESPACE.URI
        );

        appendClass(descriptor, containerElement, container.getClazz(), "upnp:class", false);

        for (DIDLObject.Class searchClass : container.getSearchClasses()) {
            appendClass(descriptor, containerElement, searchClass, "upnp:searchClass", true);
        }

        for (DIDLObject.Class createClass : container.getCreateClasses()) {
            appendClass(descriptor, containerElement, createClass, "upnp:createClass", true);
        }

        appendProperties(descriptor, containerElement, container, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI);
        appendProperties(descriptor, containerElement, container, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI);

        if (nestedItems) {
            for (Item item : container.getItems()) {
                if (item == null) continue;
                generateItem(item, descriptor, containerElement);
            }
        }

        for (Res resource : container.getResources()) {
            if (resource == null) continue;
            generateResource(resource, descriptor, containerElement);
        }

        for (DescMeta descMeta : container.getDescMetadata()) {
            if (descMeta == null) continue;
            generateDescMetadata(descMeta, descriptor, containerElement);
        }
    }

    protected void generateItem(Item item, Document descriptor, Element parent) {

        if (item.getTitle() == null) {
            throw new RuntimeException("Missing 'dc:title' element for item: " + item.getId());
        }
        if (item.getClazz() == null) {
            throw new RuntimeException("Missing 'upnp:class' element for item: " + item.getId());
        }

        Element itemElement = appendNewElement(descriptor, parent, "item");

        if (item.getId() == null)
            throw new NullPointerException("Missing id on item: " + item);
        itemElement.setAttribute("id", item.getId());

        if (item.getParentID() == null)
            throw new NullPointerException("Missing parent id on item: " + item);
        itemElement.setAttribute("parentID", item.getParentID());

        if (item.getRefID() != null)
            itemElement.setAttribute("refID", item.getRefID());
        itemElement.setAttribute("restricted", Boolean.toString(item.isRestricted()));

        appendNewElementIfNotNull(
                descriptor,
                itemElement,
                "dc:title",
                item.getTitle(),
                DIDLObject.Property.DC.NAMESPACE.URI
        );

        appendNewElementIfNotNull(
                descriptor,
                itemElement,
                "dc:creator",
                item.getCreator(),
                DIDLObject.Property.DC.NAMESPACE.URI
        );

        appendNewElementIfNotNull(
                descriptor,
                itemElement,
                "upnp:writeStatus",
                item.getWriteStatus(),
                DIDLObject.Property.UPNP.NAMESPACE.URI
        );

        appendClass(descriptor, itemElement, item.getClazz(), "upnp:class", false);

        appendProperties(descriptor, itemElement, item, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI);
        appendProperties(descriptor, itemElement, item, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI);

        for (Res resource : item.getResources()) {
            if (resource == null) continue;
            generateResource(resource, descriptor, itemElement);
        }

        for (DescMeta descMeta : item.getDescMetadata()) {
            if (descMeta == null) continue;
            generateDescMetadata(descMeta, descriptor, itemElement);
        }
    }

    protected void generateResource(Res resource, Document descriptor, Element parent) {

        if (resource.getValue() == null) {
            throw new RuntimeException("Missing resource URI value" + resource);
        }
        if (resource.getProtocolInfo() == null) {
            throw new RuntimeException("Missing resource protocol info: " + resource);
        }

        Element resourceElement = appendNewElement(descriptor, parent, "res", resource.getValue());
        resourceElement.setAttribute("protocolInfo", resource.getProtocolInfo().toString());
        if (resource.getImportUri() != null)
            resourceElement.setAttribute("importUri", resource.getImportUri().toString());
        if (resource.getSize() != null)
            resourceElement.setAttribute("size", resource.getSize().toString());
        if (resource.getDuration() != null)
            resourceElement.setAttribute("duration", resource.getDuration());
        if (resource.getBitrate() != null)
            resourceElement.setAttribute("bitrate", resource.getBitrate().toString());
        if (resource.getSampleFrequency() != null)
            resourceElement.setAttribute("sampleFrequency", resource.getSampleFrequency().toString());
        if (resource.getBitsPerSample() != null)
            resourceElement.setAttribute("bitsPerSample", resource.getBitsPerSample().toString());
        if (resource.getNrAudioChannels() != null)
            resourceElement.setAttribute("nrAudioChannels", resource.getNrAudioChannels().toString());
        if (resource.getColorDepth() != null)
            resourceElement.setAttribute("colorDepth", resource.getColorDepth().toString());
        if (resource.getProtection() != null)
            resourceElement.setAttribute("protection", resource.getProtection());
        if (resource.getResolution() != null)
            resourceElement.setAttribute("resolution", resource.getResolution());
    }

    protected void generateDescMetadata(DescMeta descMeta, Document descriptor, Element parent) {

        if (descMeta.getId() == null) {
            throw new RuntimeException("Missing id of description metadata: " + descMeta);
        }
        if (descMeta.getNameSpace() == null) {
            throw new RuntimeException("Missing namespace of description metadata: " + descMeta);
        }

        Element descElement = appendNewElement(descriptor, parent, "desc");
        descElement.setAttribute("id", descMeta.getId());
        descElement.setAttribute("nameSpace", descMeta.getNameSpace().toString());
        if (descMeta.getType() != null)
            descElement.setAttribute("type", descMeta.getType());
        populateDescMetadata(descElement, descMeta);
    }

    /**
     * Expects an <code>org.w3c.Document</code> as metadata, copies nodes of the document into the DIDL content.
     * <p>
     * This method will ignore the content and log a warning if it's of the wrong type. If you override
     * {@link #createDescMetaHandler(org.teleal.cling.support.model.DescMeta, org.teleal.common.xml.SAXParser.Handler)},
     * you most likely also want to override this method.
     * </p>
     *
     * @param descElement The DIDL content {@code <desc>} element wrapping the final metadata.
     * @param descMeta    The metadata with a <code>org.w3c.Document</code> payload.
     */
    protected void populateDescMetadata(Element descElement, DescMeta descMeta) {
        if (descMeta.getMetadata() instanceof Document) {
            Document doc = (Document) descMeta.getMetadata();

            NodeList nl = doc.getDocumentElement().getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node n = nl.item(i);
                if (n.getNodeType() != Node.ELEMENT_NODE)
                    continue;

                Node clone = descElement.getOwnerDocument().importNode(n, true);
                descElement.appendChild(clone);
            }

        } else {
            log.warning("Unknown desc metadata content, please override populateDescMetadata(): " + descMeta.getMetadata());
        }
    }

    protected void appendProperties(Document descriptor, Element parent, DIDLObject object, String prefix,
                                    Class<? extends DIDLObject.Property.NAMESPACE> namespace,
                                    String namespaceURI) {
        for (DIDLObject.Property<Object> property : object.getPropertiesByNamespace(namespace)) {
            Element el = descriptor.createElementNS(namespaceURI, prefix + ":" + property.getDescriptorName());
            parent.appendChild(el);
            property.setOnElement(el);
        }
    }

    protected void appendClass(Document descriptor, Element parent, DIDLObject.Class clazz, String element, boolean appendDerivation) {
        Element classElement = appendNewElementIfNotNull(
                descriptor,
                parent,
                element,
                clazz.getValue(),
                DIDLObject.Property.UPNP.NAMESPACE.URI
        );
        if (clazz.getFriendlyName() != null && clazz.getFriendlyName().length() > 0)
            classElement.setAttribute("name", clazz.getFriendlyName());
        if (appendDerivation)
            classElement.setAttribute("includeDerived", Boolean.toString(clazz.isIncludeDerived()));
    }

    /**
     * Sends the given string to the log with <code>Level.FINE</code>, if that log level is enabled.
     *
     * @param s The string to send to the log.
     */
    public void debugXML(String s) {
        if (log.isLoggable(Level.FINE)) {
            log.fine("-------------------------------------------------------------------------------------");
            log.fine("\n" + s);
            log.fine("-------------------------------------------------------------------------------------");
        }
    }


    /* ############################################################################################# */


    public abstract class DIDLObjectHandler<I extends DIDLObject> extends Handler<I> {

        protected DIDLObjectHandler(I instance, Handler parent) {
            super(instance, parent);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);

            if (DIDLObject.Property.DC.NAMESPACE.URI.equals(uri)) {

                if ("title".equals(localName)) {
                    getInstance().setTitle(getCharacters());
                } else if ("creator".equals(localName)) {
                    getInstance().setCreator(getCharacters());
                } else if ("description".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.DESCRIPTION(getCharacters()));
                } else if ("publisher".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.PUBLISHER(new Person(getCharacters())));
                } else if ("contributor".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.CONTRIBUTOR(new Person(getCharacters())));
                } else if ("date".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.DATE(getCharacters()));
                } else if ("language".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.LANGUAGE(getCharacters()));
                } else if ("rights".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.RIGHTS(getCharacters()));
                } else if ("relation".equals(localName)) {
                    getInstance().addProperty(new DIDLObject.Property.DC.RELATION(URI.create(getCharacters())));
                }

            } else if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) {

                if ("writeStatus".equals(localName)) {
                    try {
                        getInstance().setWriteStatus(
                                WriteStatus.valueOf(getCharacters())
                        );
                    } catch (Exception ex) {
                        log.info("Ignoring invalid writeStatus value: " + getCharacters());
                    }
                } else if ("class".equals(localName)) {
                    getInstance().setClazz(
                            new DIDLObject.Class(
                                    getCharacters(),
                                    getAttributes().getValue("name")
                            )
                    );
                } else if ("artist".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ARTIST(
                                    new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
                            )
                    );
                } else if ("actor".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ACTOR(
                                    new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
                            )
                    );
                } else if ("author".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.AUTHOR(
                                    new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
                            )
                    );
                } else if ("producer".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.PRODUCER(new Person(getCharacters()))
                    );
                } else if ("director".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.DIRECTOR(new Person(getCharacters()))
                    );
                } else if ("longDescription".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.LONG_DESCRIPTION(getCharacters())
                    );
                } else if ("storageUsed".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.STORAGE_USED(Long.valueOf(getCharacters()))
                    );
                } else if ("storageTotal".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.STORAGE_TOTAL(Long.valueOf(getCharacters()))
                    );
                } else if ("storageFree".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.STORAGE_FREE(Long.valueOf(getCharacters()))
                    );
                } else if ("storageMaxPartition".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.STORAGE_MAX_PARTITION(Long.valueOf(getCharacters()))
                    );
                } else if ("storageMedium".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.STORAGE_MEDIUM(StorageMedium.valueOrVendorSpecificOf(getCharacters()))
                    );
                } else if ("genre".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.GENRE(getCharacters())
                    );
                } else if ("album".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ALBUM(getCharacters())
                    );
                } else if ("playlist".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.PLAYLIST(getCharacters())
                    );
                } else if ("region".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.REGION(getCharacters())
                    );
                } else if ("rating".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.RATING(getCharacters())
                    );
                } else if ("toc".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.TOC(getCharacters())
                    );
                } else if ("albumArtURI".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(getCharacters()))
                    );
                } else if ("artistDiscographyURI".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ARTIST_DISCO_URI(URI.create(getCharacters()))
                    );
                } else if ("lyricsURI".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.LYRICS_URI(URI.create(getCharacters()))
                    );
                } else if ("icon".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ICON(URI.create(getCharacters()))
                    );
                } else if ("radioCallSign".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.RADIO_CALL_SIGN(getCharacters())
                    );
                } else if ("radioStationID".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.RADIO_STATION_ID(getCharacters())
                    );
                } else if ("radioBand".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.RADIO_BAND(getCharacters())
                    );
                } else if ("channelNr".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.CHANNEL_NR(Integer.valueOf(getCharacters()))
                    );
                } else if ("channelName".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.CHANNEL_NAME(getCharacters())
                    );
                } else if ("scheduledStartTime".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.SCHEDULED_START_TIME(getCharacters())
                    );
                } else if ("scheduledEndTime".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.SCHEDULED_END_TIME(getCharacters())
                    );
                } else if ("DVDRegionCode".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.DVD_REGION_CODE(Integer.valueOf(getCharacters()))
                    );
                } else if ("originalTrackNumber".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.ORIGINAL_TRACK_NUMBER(Integer.valueOf(getCharacters()))
                    );
                } else if ("userAnnotation".equals(localName)) {
                    getInstance().addProperty(
                            new DIDLObject.Property.UPNP.USER_ANNOTATION(getCharacters())
                    );
                }
            }
        }
    }

    public class RootHandler extends Handler<DIDLContent> {

        RootHandler(DIDLContent instance, SAXParser parser) {
            super(instance, parser);
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);

            if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;

            if (localName.equals("container")) {

                Container container = createContainer(attributes);
                getInstance().addContainer(container);
                createContainerHandler(container, this);

            } else if (localName.equals("item")) {

                Item item = createItem(attributes);
                getInstance().addItem(item);
                createItemHandler(item, this);

            } else if (localName.equals("desc")) {

                DescMeta desc = createDescMeta(attributes);
                getInstance().addDescMetadata(desc);
                createDescMetaHandler(desc, this);

            }
        }

        @Override
        protected boolean isLastElement(String uri, String localName, String qName) {
            if (DIDLContent.NAMESPACE_URI.equals(uri) && "DIDL-Lite".equals(localName)) {

                // Now transform all the generically typed Container and Item instances into
                // more specific Album, MusicTrack, etc. instances
                getInstance().replaceGenericContainerAndItems();

                return true;
            }
            return false;
        }
    }

    public class ContainerHandler extends DIDLObjectHandler<Container> {
        public ContainerHandler(Container instance, Handler parent) {
            super(instance, parent);
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);

            if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;

            if (localName.equals("item")) {

                Item item = createItem(attributes);
                getInstance().addItem(item);
                createItemHandler(item, this);

            } else if (localName.equals("desc")) {

                DescMeta desc = createDescMeta(attributes);
                getInstance().addDescMetadata(desc);
                createDescMetaHandler(desc, this);

            } else if (localName.equals("res")) {

                Res res = createResource(attributes);
                getInstance().addResource(res);
                createResHandler(res, this);

            }

            // We do NOT support recursive container embedded in container! The schema allows it
            // but the spec doesn't:
            //
            // Section 2.8.3: Incremental navigation i.e. the full hierarchy is never returned
            // in one call since this is likely to flood the resources available to the control
            // point (memory, network bandwidth, etc.).
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);

            if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) {

                if ("searchClass".equals(localName)) {
                    getInstance().getSearchClasses().add(
                            new DIDLObject.Class(
                                    getCharacters(),
                                    getAttributes().getValue("name"),
                                    "true".equals(getAttributes().getValue("includeDerived"))
                            )
                    );
                } else if ("createClass".equals(localName)) {
                    getInstance().getCreateClasses().add(
                            new DIDLObject.Class(
                                    getCharacters(),
                                    getAttributes().getValue("name"),
                                    "true".equals(getAttributes().getValue("includeDerived"))
                            )
                    );
                }
            }
        }

        @Override
        protected boolean isLastElement(String uri, String localName, String qName) {
            if (DIDLContent.NAMESPACE_URI.equals(uri) && "container".equals(localName)) {
                if (getInstance().getTitle() == null) {
                    throw new RuntimeException("Missing 'dc:title' element for container: " + getInstance().getId());
                }
                if (getInstance().getClazz() == null) {
                    throw new RuntimeException("Missing 'upnp:class' element for container: " + getInstance().getId());
                }
                return true;
            }
            return false;
        }
    }

    public class ItemHandler extends DIDLObjectHandler<Item> {
        public ItemHandler(Item instance, Handler parent) {
            super(instance, parent);
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);

            if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;

            if (localName.equals("res")) {

                Res res = createResource(attributes);
                getInstance().addResource(res);
                createResHandler(res, this);

            } else if (localName.equals("desc")) {

                DescMeta desc = createDescMeta(attributes);
                getInstance().addDescMetadata(desc);
                createDescMetaHandler(desc, this);

            }
        }

        @Override
        protected boolean isLastElement(String uri, String localName, String qName) {
            if (DIDLContent.NAMESPACE_URI.equals(uri) && "item".equals(localName)) {
                if (getInstance().getTitle() == null) {
                    throw new RuntimeException("Missing 'dc:title' element for item: " + getInstance().getId());
                }
                if (getInstance().getClazz() == null) {
                    throw new RuntimeException("Missing 'upnp:class' element for item: " + getInstance().getId());
                }
                return true;
            }
            return false;
        }
    }

    protected class ResHandler extends Handler<Res> {
        public ResHandler(Res instance, Handler parent) {
            super(instance, parent);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);
            getInstance().setValue(getCharacters());
        }

        @Override
        protected boolean isLastElement(String uri, String localName, String qName) {
            return DIDLContent.NAMESPACE_URI.equals(uri) && "res".equals(localName);
        }
    }

    /**
     * Extracts an <code>org.w3c.Document</code> from the nested elements in the {@code <desc>} element.
     * <p>
     * The root element of this document is a wrapper in the namespace
     * {@link org.teleal.cling.support.model.DIDLContent#DESC_WRAPPER_NAMESPACE_URI}.
     * </p>
     */
    public class DescMetaHandler extends Handler<DescMeta> {

        protected Element current;

        public DescMetaHandler(DescMeta instance, Handler parent) {
            super(instance, parent);
            instance.setMetadata(instance.createMetadataDocument());
            current = getInstance().getMetadata().getDocumentElement();
        }

        @Override
        public DescMeta<Document> getInstance() {
            return super.getInstance();
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            super.startElement(uri, localName, qName, attributes);

            Element newEl = getInstance().getMetadata().createElementNS(uri, qName);
            for (int i = 0; i < attributes.getLength(); i++) {
                newEl.setAttributeNS(
                        attributes.getURI(i),
                        attributes.getQName(i),
                        attributes.getValue(i)
                );
            }
            current.appendChild(newEl);
            current = newEl;
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);
            if (isLastElement(uri, localName, qName)) return;

            // Ignore whitespace
            if (getCharacters().length() > 0 && !getCharacters().matches("[\\t\\n\\x0B\\f\\r\\s]+"))
                current.appendChild(getInstance().getMetadata().createTextNode(getCharacters()));

            current = (Element) current.getParentNode();

            // Reset this so we can continue parsing child nodes with this handler
            characters = new StringBuilder();
            attributes = null;
        }

        @Override
        protected boolean isLastElement(String uri, String localName, String qName) {
            return DIDLContent.NAMESPACE_URI.equals(uri) && "desc".equals(localName);
        }
    }
}
