All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.mediapackage.XMLCatalogImpl Maven / Gradle / Ivy

There is a newer version: 16.7
Show newest version
/**
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.opencastproject.mediapackage;

import static com.entwinemedia.fn.Stream.$;
import static java.lang.String.format;
import static javax.xml.XMLConstants.DEFAULT_NS_PREFIX;
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE;
import static javax.xml.XMLConstants.XML_NS_URI;
import static org.opencastproject.util.EqualsUtil.hash;

import org.opencastproject.util.RequireUtil;
import org.opencastproject.util.XmlNamespaceBinding;
import org.opencastproject.util.XmlNamespaceContext;

import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.Fns;
import com.entwinemedia.fn.P2;
import com.entwinemedia.fn.fns.Booleans;

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 * This is a basic implementation for handling simple catalogs of metadata. It provides utility methods to store
 * key-value data.
 * 

* For a definition of the terms expanded name, qualified name or QName, namespace * prefix, local part and local name, please see http://www.w3.org/TR/REC-xml-names *

* By default the following namespace prefixes are bound: *

    *
  • xml - http://www.w3.org/XML/1998/namespace *
  • xmlns - http://www.w3.org/2000/xmlns/ *
  • xsi - http://www.w3.org/2001/XMLSchema-instance *
*

*

Limitations

* XMLCatalog supports only one prefix binding per namespace name, so you cannot create documents like the * following using XMLCatalog: * *
 * <root xmlns:x="http://x.demo.org" xmlns:y="http://x.demo.org">
 *   <x:elem>value</x:elem>
 *   <y:elem>value</y:elem>
 * </root>
 * 
* * However, reading of those documents is supported. */ public abstract class XMLCatalogImpl extends CatalogImpl implements XMLCatalog { private static final long serialVersionUID = -7580292199527168951L; /** Expanded name of the XML language attribute xml:lang. */ public static final EName XML_LANG_ATTR = new EName(XML_NS_URI, "lang"); /** Namespace prefix for XML schema instance. */ public static final String XSI_NS_PREFIX = "xsi"; /** To marshaling empty fields to remove existing values during merge, default is not to marshal empty elements */ protected boolean includeEmpty = false; /** * Expanded name of the XSI type attribute. *

* See http://www.w3.org/TR/xmlschema-1/#xsi_type for the * definition. */ public static final EName XSI_TYPE_ATTR = new EName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "type"); /** Key (QName) value meta data */ protected final Map> data = new HashMap<>(); /** Namespace - prefix bindings */ protected XmlNamespaceContext bindings; /** * Create an empty catalog and register the {@link javax.xml.XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI} * namespace. */ protected XMLCatalogImpl() { super(); bindings = XmlNamespaceContext.mk(XSI_NS_PREFIX, W3C_XML_SCHEMA_INSTANCE_NS_URI); } protected void addBinding(XmlNamespaceBinding binding) { bindings = bindings.add(binding); } protected XmlNamespaceContext getBindings() { return bindings; } /** * Clears the catalog. */ protected void clear() { data.clear(); } /** * Adds the element to the metadata collection. * * @param element * the expanded name of the element * @param value * the value */ protected void addElement(EName element, String value) { if (element == null) throw new IllegalArgumentException("Expanded name must not be null"); addElement(new CatalogEntry(element, value, NO_ATTRIBUTES)); } /** * Adds the element with the xml:lang attribute to the metadata collection. * * @param element * the expanded name of the element * @param value * the value * @param language * the language identifier (two letter ISO 639) */ protected void addLocalizedElement(EName element, String value, String language) { RequireUtil.notNull(element, "expanded name"); RequireUtil.notNull(language, "language"); Map attributes = new HashMap<>(1); attributes.put(XML_LANG_ATTR, language); addElement(new CatalogEntry(element, value, attributes)); } /** * Adds the element with the xsi:type attribute to the metadata collection. * * @param value * the value * @param type * the element type */ protected void addTypedElement(EName element, String value, EName type) { RequireUtil.notNull(element, "expanded name"); RequireUtil.notNull(type, "type"); Map attributes = new HashMap<>(1); attributes.put(XSI_TYPE_ATTR, toQName(type)); addElement(new CatalogEntry(element, value, attributes)); } /** * Adds an element with the xml:lang and xsi:type attributes to the metadata collection. * * @param element * the expanded name of the element * @param value * the value * @param language * the language identifier (two letter ISO 639) * @param type * the element type */ protected void addTypedLocalizedElement(EName element, String value, String language, EName type) { if (element == null) throw new IllegalArgumentException("EName name must not be null"); if (type == null) throw new IllegalArgumentException("Type must not be null"); if (language == null) throw new IllegalArgumentException("Language must not be null"); Map attributes = new HashMap<>(2); attributes.put(XML_LANG_ATTR, language); attributes.put(XSI_TYPE_ATTR, toQName(type)); addElement(new CatalogEntry(element, value, attributes)); } /** * Adds an element with attributes to the catalog. * * @param element * the expanded name of the element * @param value * the element's value * @param attributes * the attributes. May be null */ protected void addElement(EName element, String value, Attributes attributes) { if (element == null) throw new IllegalArgumentException("Expanded name must not be null"); Map attributeMap = new HashMap<>(); if (attributes != null) { for (int i = 0; i < attributes.getLength(); i++) { attributeMap.put(new EName(attributes.getURI(i), attributes.getLocalName(i)), attributes.getValue(i)); } } addElement(new CatalogEntry(element, value, attributeMap)); } /** * Adds the catalog element to the list of elements. * * @param element * the element */ private void addElement(CatalogEntry element) { // Option includeEmpty allows marshaling empty elements // for deleting existing values during a catalog merge if (element == null) return; if (StringUtils.trimToNull(element.getValue()) == null && !includeEmpty) return; List values = data.get(element.getEName()); if (values == null) { values = new ArrayList<>(); data.put(element.getEName(), values); } values.add(element); } /** * Completely removes an element. * * @param element * the expanded name of the element */ protected void removeElement(EName element) { removeValues(element, null, true); } /** * Removes all entries in a certain language from an element. * * @param element * the expanded name of the element * @param language * the language code (two letter ISO 639) or null to only remove entries without an * xml:lang attribute */ protected void removeLocalizedValues(EName element, String language) { removeValues(element, language, false); } /** * Removes values from an element or the complete element from the catalog. * * @param element * the expanded name of the element * @param language * the language code (two letter ISO 639) to remove or null to remove entries without language code * @param all * true - remove all entries for that element. This parameter overrides the language parameter. */ private void removeValues(EName element, String language, boolean all) { if (all) { data.remove(element); } else { List entries = data.get(element); if (entries != null) { for (Iterator i = entries.iterator(); i.hasNext();) { CatalogEntry entry = i.next(); if (equal(language, entry.getAttribute(XML_LANG_ATTR))) { i.remove(); } } } } } /** * Returns the values that are associated with the specified key. * * @param element * the expanded name of the element * @return the elements */ protected CatalogEntry[] getValues(EName element) { List values = data.get(element); if (values != null && values.size() > 0) { return values.toArray(new CatalogEntry[values.size()]); } return new CatalogEntry[] {}; } protected List getEntriesSorted() { return $(data.values()) .bind(Fns.>id()) .sort(catalogEntryComparator) .toList(); } /** * Returns the values that are associated with the specified key. * * @param element * the expanded name of the element * @return all values of the element or an empty list if this element does not exist or does not have any values */ @SuppressWarnings("unchecked") protected List getValuesAsList(EName element) { List values = data.get(element); return values != null ? values : Collections.EMPTY_LIST; } /** * Returns the values that are associated with the specified key. * * @param element * the expandend name of the element * @param language * a language code or null to get values without xml:lang attribute * @return all values of the element */ @SuppressWarnings("unchecked") protected List getLocalizedValuesAsList(EName element, String language) { List values = data.get(element); if (values != null) { List filtered = new ArrayList<>(); for (CatalogEntry value : values) { if (equal(language, value.getAttribute(XML_LANG_ATTR))) { filtered.add(value); } } return filtered; } else { return Collections.EMPTY_LIST; } } /** * Returns the first value that is associated with the specified name. * * @param element * the expanded name of the element * @return the first value */ protected CatalogEntry getFirstValue(EName element) { List elements = data.get(element); if (elements != null && elements.size() > 0) { return elements.get(0); } return null; } /** * Returns the first element that is associated with the specified name and attribute. * * @param element * the expanded name of the element * @param attributeEName * the expanded attribute name * @param attributeValue * the attribute value * @return the first value */ protected CatalogEntry getFirstValue(EName element, EName attributeEName, String attributeValue) { List elements = data.get(element); if (elements != null) { for (CatalogEntry entry : elements) { String v = entry.getAttribute(attributeEName); if (equal(attributeValue, v)) return entry; } } return null; } /** * Returns the first value that is associated with the specified name and language. * * @param element * the expanded name of the element * @param language * the language identifier or null to get only elements without xml:lang attribute * @return the first value */ protected CatalogEntry getFirstLocalizedValue(EName element, String language) { return getFirstValue(element, XML_LANG_ATTR, language); } /** * Returns the first value that is associated with the specified name and language. * * @param element * the expanded name of the element * @param type * the xsi:type value * @return the element */ protected CatalogEntry getFirstTypedValue(EName element, String type) { return getFirstValue(element, XSI_TYPE_ATTR, type); } /** * Tests two objects for equality. */ protected boolean equal(Object a, Object b) { return (a == null && b == null) || (a != null && a.equals(b)); } /** * Creates an xml document root and returns it. * * @return the document * @throws ParserConfigurationException * If the xml parser environment is not correctly configured */ protected Document newDocument() throws ParserConfigurationException { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); docBuilderFactory.setNamespaceAware(true); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); return docBuilder.newDocument(); } /** * Serializes the given xml document to the associated file. Please note that this method does not close the * output stream. Anyone using this method is responsible for doing it by itself. * * @param document * the document * @param docType * the document type definition (dtd) * @throws TransformerException * if serialization fails */ protected void saveToXml(Node document, String docType, OutputStream out) throws TransformerException, IOException { StreamResult streamResult = new StreamResult(out); TransformerFactory tf = TransformerFactory.newInstance(); Transformer serializer = tf.newTransformer(); serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); if (docType != null) serializer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, docType); serializer.setOutputProperty(OutputKeys.INDENT, "yes"); serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); serializer.transform(new DOMSource(document), streamResult); out.flush(); } /** * @see org.opencastproject.mediapackage.AbstractMediaPackageElement#toManifest(org.w3c.dom.Document, * org.opencastproject.mediapackage.MediaPackageSerializer) */ @Override public Node toManifest(Document document, MediaPackageSerializer serializer) throws MediaPackageException { return super.toManifest(document, serializer); } /** * Get a prefix from {@link #bindings} but throw a {@link NamespaceBindingException} if none found. */ protected String getPrefix(String namespaceURI) { final String prefix = bindings.getPrefix(namespaceURI); if (prefix != null) { return prefix; } else { throw new NamespaceBindingException(format("Namespace URI %s is not bound to a prefix", namespaceURI)); } } /** * @see org.opencastproject.mediapackage.XMLCatalog#includeEmpty(boolean) */ @Override public void includeEmpty(boolean includeEmpty) { this.includeEmpty = includeEmpty; } /** * Transform an expanded name to a qualified name based on the registered binding. * * @param eName * the expanded name to transform * @return the qualified name, e.g. dcterms:title * @throws NamespaceBindingException * if the namespace name is not bound to a prefix */ protected String toQName(EName eName) { if (eName.hasNamespace()) { return toQName(getPrefix(eName.getNamespaceURI()), eName.getLocalName()); } else { return eName.getLocalName(); } } /** * Transform an qualified name consisting of prefix and local part to an expanded name, based on the registered * binding. * * @param prefix * the prefix * @param localName * the local part * @return the expanded name * @throws NamespaceBindingException * if the namespace name is not bound to a prefix */ protected EName toEName(String prefix, String localName) { return new EName(bindings.getNamespaceURI(prefix), localName); } /** * Transform a qualified name to an expanded name, based on the registered binding. * * @param qName * the qualified name, e.g. dcterms:title or title * @return the expanded name * @throws NamespaceBindingException * if the namespace name is not bound to a prefix */ protected EName toEName(String qName) { String[] parts = splitQName(qName); return new EName(bindings.getNamespaceURI(parts[0]), parts[1]); } /** * Splits a QName into its parts. * * @param qName * the qname to split * @return an array of prefix (0) and local part (1). The prefix is "" if the qname belongs to the default namespace. */ private static String[] splitQName(String qName) { final String[] parts = qName.split(":", 3); switch (parts.length) { case 1: return new String[] { DEFAULT_NS_PREFIX, parts[0] }; case 2: return parts; default: throw new IllegalArgumentException("Local name must not contain ':'"); } } /** * Returns a "prefixed name" consisting of namespace prefix and local name. * * @param prefix * the namespace prefix, may be null * @param localName * the local name * @return the "prefixed name" prefix:localName */ private static String toQName(String prefix, String localName) { final StringBuilder b = new StringBuilder(); if (prefix != null && !DEFAULT_NS_PREFIX.equals(prefix)) { b.append(prefix); b.append(":"); } b.append(localName); return b.toString(); } // -------------------------------------------------------------------------------------------- private static final Map NO_ATTRIBUTES = new HashMap<>(); CatalogEntry mkCatalogEntry(EName name, String value, Map attributes) { return new CatalogEntry(name, value, attributes); } /** * Element representation. */ public final class CatalogEntry implements XmlElement, Comparable, Serializable { /** The serial version UID */ private static final long serialVersionUID = 7195298081966562710L; private final EName name; private final String value; /** The attributes of this element */ private final Map attributes; /** * Creates a new catalog element representation with name, value and attributes. * * @param value * the element value * @param attributes * the element attributes */ public CatalogEntry(EName name, String value, Map attributes) { this.name = name; this.value = value; this.attributes = new HashMap<>(attributes); } /** * Returns the qualified name of the entry as a string. The namespace of the entry has to be bound to a prefix for * this method to succeed. */ public String getQName() { return toQName(name); } /** * Returns the expanded name of the entry. */ public EName getEName() { return name; } /** * Returns the element value. * * @return the value */ public String getValue() { return value; } /** * Returns true if the element contains attributes. * * @return true if the element contains attributes */ public boolean hasAttributes() { return attributes.size() > 0; } /** * Returns the element's attributes. * * @return the attributes */ public Map getAttributes() { return Collections.unmodifiableMap(attributes); } /** * Returns true if the element contains an attribute with the given name. * * @return true if the element contains the attribute */ public boolean hasAttribute(EName name) { return attributes.containsKey(name); } /** * Returns the attribute value for the given attribute. * * @return the attribute or null */ public String getAttribute(EName name) { return attributes.get(name); } @Override public int hashCode() { return hash(name, value); } @Override public boolean equals(Object that) { return (this == that) || (that instanceof CatalogEntry && eqFields((CatalogEntry) that)); } private boolean eqFields(CatalogEntry that) { return this.compareTo(that) == 0; } /** * Returns the XML representation of this entry. * * @param document * the document * @return the xml node */ @Override public Node toXml(Document document) { Element node = document.createElement(toQName(name)); // Write prefix binding to document root element bindNamespaceFor(document, name); List keySet = new ArrayList<>(attributes.keySet()); Collections.sort(keySet); for (EName attrEName : keySet) { String value = attributes.get(attrEName); if (attrEName.hasNamespace()) { // Write prefix binding to document root element bindNamespaceFor(document, attrEName); if (XSI_TYPE_ATTR.equals(attrEName)) { // Special treatment for xsi:type attributes try { EName typeName = toEName(value); bindNamespaceFor(document, typeName); } catch (NamespaceBindingException ignore) { // Type is either not a QName or its namespace is not bound. // We decide to gently ignore those cases. } } } node.setAttribute(toQName(attrEName), value); } if (value != null) { node.appendChild(document.createTextNode(value)); } return node; } /** * Compare two catalog entries. Comparison order: * - e_name * - number of attributes (less come first) * - attribute comparison (e_name -> value) */ @Override public int compareTo(CatalogEntry o) { int c; c = getEName().compareTo(o.getEName()); if (c != 0) { return c; } else { // compare attributes c = attributes.size() - o.attributes.size(); if (c != 0) { return c; } else { return $(attributes.entrySet()).sort(attributeComparator) .zip($(o.attributes.entrySet()).sort(attributeComparator)) .map(new Fn, Entry>, Integer>() { @Override public Integer apply(P2, Entry> as) { return attributeComparator.compare(as.get1(), as.get2()); } }) .find(Booleans.ne(0)) .getOr(0); } } } /** * Writes a namespace binding for catalog entry name to the documents root element. * xmlns:prefix="namespace" */ private void bindNamespaceFor(Document document, EName name) { Element root = (Element) document.getFirstChild(); String namespace = name.getNamespaceURI(); // Do not bind the "xml" namespace. It is bound by default if (!XML_NS_URI.equals(namespace)) { root.setAttribute(XMLNS_ATTRIBUTE + ":" + XMLCatalogImpl.this.getPrefix(name.getNamespaceURI()), name.getNamespaceURI()); } } @Override public String toString() { return value; } } static int doCompareTo(EName k1, String v1, EName k2, String v2) { final int c = k1.compareTo(k2); return c != 0 ? c : v1.compareTo(v2); } private static final Comparator> attributeComparator = new Comparator>() { @Override public int compare(Entry o1, Entry o2) { return doCompareTo(o1.getKey(), o1.getValue(), o2.getKey(), o2.getValue()); } }; private static final Comparator catalogEntryComparator = new Comparator() { @Override public int compare(CatalogEntry o1, CatalogEntry o2) { return o1.compareTo(o2); } }; // -------------------------------------------------------------------------------------------- // -- /** * {@inheritDoc} * * @see org.opencastproject.mediapackage.XMLCatalog#toXml(java.io.OutputStream, boolean) */ @Override public void toXml(OutputStream out, boolean format) throws IOException { try { Document doc = this.toXml(); DOMSource domSource = new DOMSource(doc); StreamResult result = new StreamResult(out); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(domSource, result); } catch (ParserConfigurationException e) { throw new IOException("unable to parse document"); } catch (TransformerException e) { throw new IOException("unable to transform dom to a stream"); } } /** * {@inheritDoc} * * @see org.opencastproject.mediapackage.XMLCatalog#toXmlString() */ @Override public String toXmlString() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); toXml(out, true); return new String(out.toByteArray(), StandardCharsets.UTF_8); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy