/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jempbox.xmp;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.jempbox.impl.DateConverter;
import org.apache.jempbox.impl.XMLUtil;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This class represents a metadata schema that can be stored in an XMP
 * document. It handles all generic properties that are available. See
 * subclasses for access to specific properties.
 * 
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * 
 */
public class XMPSchema
{
    /**
     * The standard xmlns namespace.
     */
    public static final String NS_NAMESPACE = "http://www.w3.org/2000/xmlns/";

    /**
     * The XML schema prefix.
     */
    protected String prefix;

    /**
     * The DOM representation of this object.
     */
    protected Element schema = null;

    /**
     * Create a new blank schema that can be populated.
     * 
     * @param parent
     *            The parent XMP document that this schema will be part of.
     * @param namespaceName
     *            The name of the namespace, ie pdf,dc,...
     * @param namespaceURI
     *            The URI of the namespace, ie "http://ns.adobe.com/pdf/1.3/"
     */
    public XMPSchema(XMPMetadata parent, String namespaceName,
            String namespaceURI)
    {
        schema = parent.xmpDocument.createElementNS(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
                "rdf:Description");
        prefix = namespaceName;
        schema.setAttributeNS(NS_NAMESPACE, "xmlns:" + namespaceName,
                namespaceURI);
    }

    /**
     * Create schema from an existing XML element.
     * 
     * @param element
     *            The existing XML element.
     * @param aPrefix
     *            The XML prefix.
     */
    public XMPSchema(Element element, String aPrefix)
    {
        schema = element;
        if (aPrefix != null)
        {
            prefix = aPrefix;
        }
        else
        {
            prefix = "";
        }
    }

    /**
     * Get the XML element that is represented by this schema.
     * 
     * @return The root XML element of this schema.
     */
    public Element getElement()
    {
        return schema;
    }

    /**
     * Get the RDF about attribute.
     * 
     * @return The RDF 'about' attribute.
     */
    public String getAbout()
    {
        return getTextProperty("rdf:about");
    }

    /**
     * Set the RDF 'about' attribute. Passing in null will clear this attribute.
     * 
     * @param about
     *            The new RFD about value.
     */
    public void setAbout(String about)
    {
        if (about == null)
        {
            schema.removeAttribute("rdf:about");
        }
        else
        {
            schema.setAttribute("rdf:about", about);
        }
    }

    /**
     * Set a simple text property on the schema.
     * 
     * @param propertyName
     *            The name of the property, it must contain the namespace
     *            prefix, ie "pdf:Keywords"
     * @param propertyValue
     *            The value for the property, can be any string. Passing null
     *            will remove the property.
     */
    public void setTextProperty(String propertyName, String propertyValue)
    {
        if (propertyValue == null)
        {
            schema.removeAttribute(propertyName);
            NodeList keywordList = schema.getElementsByTagName(propertyName);
            for (int i = 0; i < keywordList.getLength(); i++)
            {
                schema.removeChild(keywordList.item(i));
            }

        }
        else
        {
            if (schema.hasAttribute(propertyName))
            {
                schema.setAttribute(propertyName, propertyValue);
            }
            else
            {
                if (schema.hasChildNodes())
                {
                    NodeList nodeList = schema
                            .getElementsByTagName(propertyName);
                    if (nodeList.getLength() > 0)
                    {
                        Element node = (Element) nodeList.item(0);
                        node.setNodeValue(propertyValue);
                    }
                    else
                    {
                        Element textNode = schema.getOwnerDocument()
                                .createElement(propertyName);
                        XMLUtil.setStringValue(textNode, propertyValue);
                        schema.appendChild(textNode);
                    }
                }
                else
                {
                    schema.setAttribute(propertyName, propertyValue);
                }
            }
        }
    }

    /**
     * Get the value of a simple text property.
     * 
     * @param propertyName
     *            The name of the property to get, it must include the namespace
     *            prefix. ie "pdf:Keywords".
     * 
     * @return The value of the text property or the null if there is no value.
     */
    public String getTextProperty(String propertyName)
    {
        // propertyValue == null does not work, since getAttribute returns the
        // empty string if the attribute is not found

        if (schema.hasAttribute(propertyName))
        {
            return schema.getAttribute(propertyName);
        }
        else
        {
            NodeList nodes = schema.getElementsByTagName(propertyName);
            if (nodes.getLength() > 0)
            {
                Element node = (Element) nodes.item(0);
                return XMLUtil.getStringValue(node);
            }
            return null;
        }
    }

    /**
     * Get the value of the property as a date.
     * 
     * @param propertyName
     *            The fully qualified property name for the date.
     * 
     * @return The value of the property as a date.
     * 
     * @throws IOException
     *             If there is an error converting the value to a date.
     */
    public Calendar getDateProperty(String propertyName) throws IOException
    {
        return DateConverter.toCalendar(getTextProperty(propertyName));
    }

    /**
     * Set the value of the property as a date.
     * 
     * @param propertyName
     *            The fully qualified property name for the date.
     * @param date
     *            The date to set, or null to clear.
     */
    public void setDateProperty(String propertyName, Calendar date)
    {
        if (date != null)
        {
            setTextProperty(propertyName, DateConverter.toISO8601(date));
        }
        else
        {
            // remove the value for the given property 
            setTextProperty(propertyName, null);
        }
    }

    /**
     * Get the value of the property as a boolean.
     * 
     * @param propertyName
     *            The fully qualified property name for the boolean.
     * 
     * @return The value of the property as a boolean.
     */
    public Boolean getBooleanProperty(String propertyName)
    {
        Boolean value = null;
        String stringValue = getTextProperty(propertyName);
        if (stringValue != null)
        {
            value = stringValue.equals("True") ? Boolean.TRUE : Boolean.FALSE;
        }
        return value;
    }

    /**
     * Set the value of the property as a boolean.
     * 
     * @param propertyName
     *            The fully qualified property name for the boolean.
     * @param bool
     *            The boolean to set, or null to clear.
     */
    public void setBooleanProperty(String propertyName, Boolean bool)
    {
        String value = null;
        if (bool != null)
        {
            value = bool.booleanValue() ? "True" : "False";
        }
        setTextProperty(propertyName, value);
    }

    /**
     * Get the value of the property as an integer.
     * 
     * @param propertyName
     *            The fully qualified property name for the integer.
     * 
     * @return The value of the property as an integer.
     */
    public Integer getIntegerProperty(String propertyName)
    {
        Integer retval = null;
        String intProperty = getTextProperty(propertyName);
        if (intProperty != null && intProperty.length() > 0)
        {
            retval = new Integer(intProperty);
        }
        return retval;
    }

    /**
     * Set the value of the property as an integer.
     * 
     * @param propertyName
     *            The fully qualified property name for the integer.
     * @param intValue
     *            The int to set, or null to clear.
     */
    public void setIntegerProperty(String propertyName, Integer intValue)
    {
        String textValue = null;
        if (intValue != null)
        {
            textValue = intValue.toString();
        }
        setTextProperty(propertyName, textValue);
    }

    /**
     * Remove all matching entries with the given value from the bag.
     * 
     * @param bagName
     *            The name of the bag, it must include the namespace prefix. ie
     *            "pdf:Keywords".
     * @param bagValue
     *            The value to remove from the bagList.
     */
    public void removeBagValue(String bagName, String bagValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(bagName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Bag");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
                NodeList items = bagElement.getElementsByTagName("rdf:li");
                for (int i = items.getLength() - 1; i >= 0; i--)
                {
                    Element li = (Element) items.item(i);
                    String value = XMLUtil.getStringValue(li);
                    if (value.equals(bagValue))
                    {
                        bagElement.removeChild(li);
                    }
                }
            }
        }
    }

    /**
     * Add an entry to a bag property.
     * 
     * @param bagName
     *            The name of the bag, it must include the namespace prefix. ie
     *            "pdf:Keywords".
     * @param bagValue
     *            The value to add to the bagList.
     */
    public void addBagValue(String bagName, String bagValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(bagName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Bag");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
            }
        }
        else
        {
            Element contElement = schema.getOwnerDocument().createElement(
                    bagName);
            schema.appendChild(contElement);
            bagElement = schema.getOwnerDocument().createElement("rdf:Bag");
            contElement.appendChild(bagElement);
        }
        Element liElement = schema.getOwnerDocument().createElement("rdf:li");
        XMLUtil.setStringValue(liElement, bagValue);
        if (bagElement != null)
        {
            bagElement.appendChild(liElement);
        }
    }

    /**
     * Get all the values of the bag property. This will return a list of
     * java.lang.String objects, this is a read-only list.
     * 
     * @param bagName
     *            The name of the bag property to get, it must include the
     *            namespace prefix. ie "pdf:Keywords"
     * 
     * @return All of the values of the bag property in a list.
     */
    public List<String> getBagList(String bagName)
    {
        List<String> retval = null;
        NodeList nodes = schema.getElementsByTagName(bagName);
        if (nodes.getLength() > 0)
        {
            Element contributor = (Element) nodes.item(0);
            NodeList bagList = contributor.getElementsByTagName("rdf:Bag");
            if (bagList.getLength() > 0)
            {
                Element bag = (Element) bagList.item(0);
                retval = new ArrayList<String>();
                NodeList items = bag.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength(); i++)
                {
                    Element li = (Element) items.item(i);
                    retval.add(XMLUtil.getStringValue(li));
                }
                retval = Collections.unmodifiableList(retval);
            }
        }

        return retval;
    }

    /**
     * Remove all matching values from a sequence property.
     * 
     * @param seqName
     *            The name of the sequence property. It must include the
     *            namespace prefix. ie "pdf:Keywords".
     * @param seqValue
     *            The value to remove from the list.
     */
    public void removeSequenceValue(String seqName, String seqValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
                NodeList items = bagElement.getElementsByTagName("rdf:li");
                for (int i = items.getLength() - 1; i >= 0; i--)
                {
                    Element li = (Element) items.item(i);
                    String value = XMLUtil.getStringValue(li);
                    if (value.equals(seqValue))
                    {
                        bagElement.removeChild(li);
                    }
                }
            }
        }
    }

    /**
     * Remove a value from a sequence property. This will remove all entries
     * from the list.
     * 
     * @param seqName
     *            The name of the sequence property. It must include the
     *            namespace prefix. ie "pdf:Keywords".
     * @param seqValue
     *            The value to remove from the list.
     */
    public void removeSequenceValue(String seqName, Elementable seqValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
                NodeList items = bagElement.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength(); i++)
                {
                    Element li = (Element) items.item(i);
                    if (li == seqValue.getElement())
                    {
                        bagElement.removeChild(li);
                    }
                }
            }
        }
    }

    /**
     * Add a new value to a sequence property.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords"
     * @param seqValue
     *            The value to add to the sequence.
     */
    public void addSequenceValue(String seqName, String seqValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
            }
            else
            {
                // xml is crap discard it
                schema.removeChild(nodes.item(0));
            }
        }
        if (bagElement == null)
        {
            Element contElement = schema.getOwnerDocument().createElement(
                    seqName);
            schema.appendChild(contElement);
            bagElement = schema.getOwnerDocument().createElement("rdf:Seq");
            contElement.appendChild(bagElement);
        }
        Element liElement = schema.getOwnerDocument().createElement("rdf:li");
        liElement.appendChild(schema.getOwnerDocument()
                .createTextNode(seqValue));
        bagElement.appendChild(liElement);
    }

    /**
     * Add a new value to a sequence property.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords"
     * @param seqValue
     *            The value to add to the sequence.
     */
    public void addSequenceValue(String seqName, Elementable seqValue)
    {
        Element bagElement = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contElement = (Element) nodes.item(0);
            NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                bagElement = (Element) bagList.item(0);
            }
        }
        else
        {
            Element contElement = schema.getOwnerDocument().createElement(
                    seqName);
            schema.appendChild(contElement);
            bagElement = schema.getOwnerDocument().createElement("rdf:Seq");
            contElement.appendChild(bagElement);
        }
        if (bagElement != null)
        {
            bagElement.appendChild(seqValue.getElement());
        }
    }

    /**
     * Get all the values in a sequence property.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords".
     * 
     * @return A read-only list of java.lang.String objects or null if the
     *         property does not exist.
     */
    public List<String> getSequenceList(String seqName)
    {
        List<String> retval = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contributor = (Element) nodes.item(0);
            NodeList bagList = contributor.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                Element bag = (Element) bagList.item(0);
                retval = new ArrayList<String>();
                NodeList items = bag.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength(); i++)
                {
                    Element li = (Element) items.item(i);
                    retval.add(XMLUtil.getStringValue(li));
                }
                retval = Collections.unmodifiableList(retval);
            }
        }
        return retval;
    }

    /**
     * Get a list of ResourceEvent objects.
     * 
     * @param seqName
     *            The name of the sequence to retrieve.
     * 
     * @return A list of ResourceEvent objects or null if they do not exist.
     */
    public List<ResourceEvent> getEventSequenceList(String seqName)
    {
        List<ResourceEvent> retval = null;
        NodeList nodes = schema.getElementsByTagName(seqName);
        if (nodes.getLength() > 0)
        {
            Element contributor = (Element) nodes.item(0);
            NodeList bagList = contributor.getElementsByTagName("rdf:Seq");
            if (bagList.getLength() > 0)
            {
                Element bag = (Element) bagList.item(0);
                retval = new ArrayList<ResourceEvent>();
                NodeList items = bag.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength(); i++)
                {
                    Element li = (Element) items.item(i);
                    retval.add(new ResourceEvent(li));
                }
                retval = Collections.unmodifiableList(retval);
            }
        }
        return retval;
    }

    /**
     * Remove a date sequence value from the list.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords"
     * @param date
     *            The date to remove from the sequence property.
     */
    public void removeSequenceDateValue(String seqName, Calendar date)
    {
        String dateAsString = DateConverter.toISO8601(date);
        removeSequenceValue(seqName, dateAsString);
    }

    /**
     * Add a date sequence value to the list.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords"
     * @param date
     *            The date to add to the sequence property.
     */
    public void addSequenceDateValue(String seqName, Calendar date)
    {
        String dateAsString = DateConverter.toISO8601(date);
        addSequenceValue(seqName, dateAsString);
    }

    /**
     * Get all the date values in a sequence property.
     * 
     * @param seqName
     *            The name of the sequence property, it must include the
     *            namespace prefix. ie "pdf:Keywords".
     * 
     * @return A read-only list of java.util.Calendar objects or null if the
     *         property does not exist.
     * 
     * @throws IOException
     *             If there is an error converting the value to a date.
     */
    public List<Calendar> getSequenceDateList(String seqName) throws IOException
    {
        List<String> strings = getSequenceList(seqName);
        List<Calendar> retval = null;
        if (strings != null)
        {
            retval = new ArrayList<Calendar>();
            for (int i = 0; i < strings.size(); i++)
            {
                retval.add(DateConverter.toCalendar(strings.get(i)));
            }
        }
        return retval;
    }

    /**
     * Set the value of a multi-lingual property.
     * 
     * @param propertyName
     *            The name of the property, it must include the namespace
     *            prefix. ie "pdf:Keywords"
     * @param language
     *            The language code of the value. If null then "x-default" is
     *            assumed.
     * @param value
     *            The value of the property in the specified language.
     */
    public void setLanguageProperty(String propertyName, String language,
            String value)
    {
        NodeList nodes = schema.getElementsByTagName(propertyName);
        Element property = null;
        if (nodes.getLength() == 0)
        {
            if (value == null)
            {
                // value is null, it doesn't already exist so there
                // is nothing to do.
                return;
            }
            property = schema.getOwnerDocument().createElement(propertyName);
            schema.appendChild(property);
        }
        else
        {
            property = (Element) nodes.item(0);
        }
        Element alt = null;
        NodeList altList = property.getElementsByTagName("rdf:Alt");
        if (altList.getLength() == 0)
        {
            if (value == null)
            {
                // value is null, it doesn't already exist so there
                // is nothing to do.
                return;
            }
            alt = schema.getOwnerDocument().createElement("rdf:Alt");
            property.appendChild(alt);
        }
        else
        {
            alt = (Element) altList.item(0);
        }
        NodeList items = alt.getElementsByTagName("rdf:li");
        if (language == null)
        {
            language = "x-default";
        }
        boolean foundValue = false;
        for (int i = 0; i < items.getLength(); i++)
        {
            Element li = (Element) items.item(i);
            if (value == null)
            {
                alt.removeChild(li);
            }
            else if (language.equals(li.getAttribute("xml:lang")))
            {
                foundValue = true;
                XMLUtil.setStringValue(li, value);
            }
        }
        if (value != null && !foundValue)
        {
            Element li = schema.getOwnerDocument().createElement("rdf:li");
            li.setAttribute("xml:lang", language);
            XMLUtil.setStringValue(li, value);
            if (language.equals("x-default"))
            {
                // default should be first element, see XMP spec
                alt.insertBefore(li, alt.getFirstChild());
            }
            else
            {
                alt.appendChild(li);
            }

        }
    }

    /**
     * Get the value of a multi-lingual property.
     * 
     * @param propertyName
     *            The name of the property, it must include the namespace
     *            prefix. ie "pdf:Keywords"
     * @param language
     *            The language code of the value. If null then "x-default" is
     *            assumed.
     * 
     * @return The value of the language property.
     */
    public String getLanguageProperty(String propertyName, String language)
    {
        String retval = null;
        if (language == null)
        {
            language = "x-default";
        }

        NodeList nodes = schema.getElementsByTagName(propertyName);
        if (nodes.getLength() > 0)
        {
            Element property = (Element) nodes.item(0);
            NodeList altList = property.getElementsByTagName("rdf:Alt");
            if (altList.getLength() > 0)
            {
                Element alt = (Element) altList.item(0);
                NodeList items = alt.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength() && retval == null; i++)
                {
                    Element li = (Element) items.item(i);
                    String elementLanguage = li.getAttribute("xml:lang");
                    if (language.equals(elementLanguage))
                    {
                        retval = XMLUtil.getStringValue(li);
                    }
                }
            }
            else if (property.getChildNodes().getLength() == 1 
                    && Node.TEXT_NODE == property.getFirstChild().getNodeType())
            {
                retval = property.getFirstChild().getNodeValue();
            }
        }
        return retval;
    }

    /**
     * Set the value of a multi-lingual property.
     * 
     * @param propertyName
     *            The name of the property, it must include the namespace
     *            prefix. ie "pdf:Keywords"
     * @param language
     *            The language code of the value. If null then "x-default" is
     *            assumed.
     * @param value
     *            The value of the property in the specified language.
     */
    public void setThumbnailProperty(String propertyName, String language,
            Thumbnail value)
    {
        NodeList nodes = schema.getElementsByTagName(propertyName);
        Element property = null;
        if (nodes.getLength() == 0)
        {
            if (value == null)
            {
                // value is null, it doesn't already exist so there
                // is nothing to do.
                return;
            }
            property = schema.getOwnerDocument().createElement(propertyName);
            schema.appendChild(property);
        }
        else
        {
            property = (Element) nodes.item(0);
        }
        Element alt = null;
        NodeList altList = property.getElementsByTagName("rdf:Alt");
        if (altList.getLength() == 0)
        {
            if (value == null)
            {
                // value is null, it doesn't already exist so there
                // is nothing to do.
                return;
            }
            alt = schema.getOwnerDocument().createElement("rdf:Alt");
            property.appendChild(alt);
        }
        else
        {
            alt = (Element) altList.item(0);
        }
        NodeList items = alt.getElementsByTagName("rdf:li");
        if (language == null)
        {
            language = "x-default";
        }
        boolean foundValue = false;
        for (int i = 0; i < items.getLength(); i++)
        {
            Element li = (Element) items.item(i);
            if (value == null)
            {
                alt.removeChild(li);
            }
            else if (language.equals(li.getAttribute("xml:lang")))
            {
                foundValue = true;
                alt.replaceChild(li, value.getElement());
            }
        }
        if (value != null && !foundValue)
        {
            Element li = value.getElement();
            li.setAttribute("xml:lang", language);
            if (language.equals("x-default"))
            {
                // default should be first element, see XMP spec
                alt.insertBefore(li, alt.getFirstChild());
            }
            else
            {
                alt.appendChild(li);
            }

        }
    }

    /**
     * Get the value of a multi-lingual property.
     * 
     * @param propertyName
     *            The name of the property, it must include the namespace
     *            prefix. ie "pdf:Keywords"
     * @param language
     *            The language code of the value. If null then "x-default" is
     *            assumed.
     * 
     * @return The value of the language property.
     */
    public Thumbnail getThumbnailProperty(String propertyName, String language)
    {
        Thumbnail retval = null;
        if (language == null)
        {
            language = "x-default";
        }

        NodeList nodes = schema.getElementsByTagName(propertyName);
        if (nodes.getLength() > 0)
        {
            Element property = (Element) nodes.item(0);
            NodeList altList = property.getElementsByTagName("rdf:Alt");
            if (altList.getLength() > 0)
            {
                Element alt = (Element) altList.item(0);
                NodeList items = alt.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength() && retval == null; i++)
                {
                    Element li = (Element) items.item(i);
                    String elementLanguage = li.getAttribute("xml:lang");
                    if (language.equals(elementLanguage))
                    {
                        retval = new Thumbnail(li);
                    }
                }
            }
        }
        return retval;
    }

    /**
     * Get a list of all languages that are currently defined for a specific
     * property.
     * 
     * @param propertyName
     *            The name of the property, it must include the namespace
     *            prefix. ie "pdf:Keywords"
     * 
     * @return A list of all languages, this will return an non-null empty list
     *         if none have been defined.
     */
    public List<String> getLanguagePropertyLanguages(String propertyName)
    {
        List<String> retval = new ArrayList<String>();

        NodeList nodes = schema.getElementsByTagName(propertyName);
        if (nodes.getLength() > 0)
        {
            Element property = (Element) nodes.item(0);
            NodeList altList = property.getElementsByTagName("rdf:Alt");
            if (altList.getLength() > 0)
            {
                Element alt = (Element) altList.item(0);
                NodeList items = alt.getElementsByTagName("rdf:li");
                for (int i = 0; i < items.getLength(); i++)
                {
                    Element li = (Element) items.item(i);
                    String elementLanguage = li.getAttribute("xml:lang");
                    if (elementLanguage == null)
                    {
                        retval.add("x-default");
                    }
                    else
                    {
                        retval.add(elementLanguage);
                    }
                }
            }
        }
        return retval;
    }

    /**
     * A basic schema merge, it merges bags and sequences and replace everything
     * else.
     * 
     * @param xmpSchema The schema to merge.
     * @throws IOException If there is an error during the merge.
     */
    public void merge(XMPSchema xmpSchema) throws IOException
    {
        if (!xmpSchema.getClass().equals(this.getClass()))
        {
            throw new IOException("Can only merge schemas of the same type.");
        }

        NamedNodeMap attributes = xmpSchema.getElement().getAttributes();
        for (int i = 0; i < attributes.getLength(); i++)
        {
            Node a = attributes.item(i);
            String name = a.getNodeName();
            if (name.startsWith(prefix))
            {
                String newValue = xmpSchema.getTextProperty(name);
                setTextProperty(name, newValue);
            }
        }
        NodeList nodes = xmpSchema.getElement().getChildNodes();

        for (int i = 0; i < nodes.getLength(); i++)
        {
            Node a = nodes.item(i);
            String name = a.getNodeName();
            if (name.startsWith(prefix))
            {
                if (a instanceof Element)
                {
                    Element e = (Element) a;
                    if (nodes.getLength() > 0)
                    {
                        NodeList seqList = e.getElementsByTagName("rdf:Seq");
                        if (seqList.getLength() > 0)
                        {
                            List<String> newList = xmpSchema.getSequenceList(name);
                            List<String> oldList = getSequenceList(name);

                            Iterator<String> it = newList.iterator();

                            while (it.hasNext())
                            {
                                String object = it.next();
                                if (oldList == null
                                        || !oldList.contains(object))
                                {
                                    addSequenceValue(name, object);
                                }
                            }
                            continue;
                        }
                        NodeList bagList = e.getElementsByTagName("rdf:Bag");
                        if (bagList.getLength() > 0)
                        {
                            List<String> newList = xmpSchema.getBagList(name);
                            List<String> oldList = getBagList(name);

                            Iterator<String> it = newList.iterator();

                            while (it.hasNext())
                            {
                                String object = it.next();
                                if (oldList == null
                                        || !oldList.contains(object))
                                {
                                    addBagValue(name, object);
                                }
                            }
                            continue;
                        }
                    }
                }
                String newValue = xmpSchema.getTextProperty(name);
                setTextProperty(name, newValue);
            }
        }
    }
}