/** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.collect.io; import static com.opengamma.strata.collect.Guavate.ensureOnlyOne; import static com.opengamma.strata.collect.Guavate.toImmutableList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.xml.namespace.QName; import org.joda.beans.ImmutableBean; import org.joda.beans.MetaBean; import org.joda.beans.Property; import org.joda.beans.PropertyDefinition; import org.joda.beans.impl.light.LightMetaBean; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.Messages; /** * A single element in the tree structure of XML. * <p> * This class is a minimal, lightweight representation of an element in the XML tree. * The element has a name, attributes, and either content or children. * <p> * Note that this representation does not express all XML features. * No support is provided for processing instructions, comments or mixed content. * In addition, it is not possible to determine the difference between empty content and no children. * <p> * There is no explicit support for namespaces. * When creating instances, the caller may choose to use a convention to represent namespaces. * For example, element and attribute names may use prefixes or the standard {@link QName} format. */ public final class XmlElement implements ImmutableBean { /** * The meta-bean. * This is a manually coded bean. */ private static MetaBean META_BEAN = LightMetaBean.of(XmlElement.class); /** * The element name. */ @PropertyDefinition(validate = "notNull") private final String name; /** * The attributes. */ @PropertyDefinition(validate = "notNull") private final ImmutableMap<String, String> attributes; /** * The element content. */ @PropertyDefinition(validate = "notNull") private final String content; /** * The child nodes. */ @PropertyDefinition(validate = "notNull") private final ImmutableList<XmlElement> children; //------------------------------------------------------------------------- /** * Obtains an instance with content and no attributes. * <p> * Returns an element representing XML with content, but no children. * * @param name the element name, not empty * @param content the content, empty if the element has no content * @return the element */ public static XmlElement ofContent(String name, String content) { return ofContent(name, ImmutableMap.of(), content); } /** * Obtains an instance with content and attributes. * <p> * Returns an element representing XML with content and attributes but no children. * * @param name the element name, not empty * @param attributes the attributes, empty if the element has no attributes * @param content the content, empty if the element has no content * @return the element */ public static XmlElement ofContent(String name, Map<String, String> attributes, String content) { return new XmlElement(name, ImmutableMap.copyOf(attributes), content, ImmutableList.of()); } /** * Obtains an instance with children and no attributes. * <p> * Returns an element representing XML with children, but no content. * * @param name the element name, not empty * @param children the children, empty if the element has no children * @return the element */ public static XmlElement ofChildren(String name, List<XmlElement> children) { return ofChildren(name, ImmutableMap.of(), children); } /** * Obtains an instance with children and attributes. * <p> * Returns an element representing XML with children and attributes, but no content. * * @param name the element name, not empty * @param attributes the attributes, empty if the element has no attributes * @param children the children, empty if the element has no children * @return the element */ public static XmlElement ofChildren(String name, Map<String, String> attributes, List<XmlElement> children) { return new XmlElement(name, ImmutableMap.copyOf(attributes), "", ImmutableList.copyOf(children)); } //------------------------------------------------------------------------- /** * Creates an instance. * * @param name the element name, not empty * @param attributes the attributes, empty if the element has no attributes * @param content the content, empty if the element has no content * @param children the children, empty if the element has no children */ private XmlElement( String name, ImmutableMap<String, String> attributes, String content, ImmutableList<XmlElement> children) { this.name = ArgChecker.notEmpty(name, "name"); this.attributes = ArgChecker.notNull(attributes, "attributes"); this.content = ArgChecker.notNull(content, "content"); this.children = ArgChecker.notNull(children, "children"); } //----------------------------------------------------------------------- /** * Gets the element name. * * @return the name */ public String getName() { return name; } /** * Gets an attribute by name, throwing an exception if not found. * <p> * This returns the value of the attribute with the specified name. * An exception is thrown if the attribute does not exist. * * @param attrName the attribute name to find * @return the attribute value * @throws IllegalArgumentException if the attribute name does not exist */ public String getAttribute(String attrName) { String attrValue = attributes.get(attrName); if (attrValue == null) { throw new IllegalArgumentException(Messages.format( "Unknown attribute '{}' on element '{}'", attrName, name)); } return attrValue; } /** * Finds an attribute by name, or empty if not found. * <p> * This returns the value of the attribute with the specified name. * If the attribute is not found, optional empty is returned. * * @param attrName the attribute name to find * @return the attribute value, optional */ public Optional<String> findAttribute(String attrName) { return Optional.ofNullable(attributes.get(attrName)); } /** * Gets the attributes. * <p> * This returns all the attributes of this element. * * @return the attributes */ public ImmutableMap<String, String> getAttributes() { return attributes; } /** * Checks if the element has content. * <p> * Content exists if it is non-empty. * * @return the content */ public boolean hasContent() { return content.length() > 0; } /** * Gets the element content. * <p> * If this element has no content, the empty string is returned. * * @return the content */ public String getContent() { return content; } /** * Gets a child element by index. * * @param index the index to find * @return the child * @throws IndexOutOfBoundsException if the index is invalid */ public XmlElement getChild(int index) { return children.get(index); } /** * Gets the child elements. * <p> * This returns all the children of this element. * * @return the children */ public ImmutableList<XmlElement> getChildren() { return children; } /** * Gets the child element with the specified name, throwing an exception if not found or more than one. * <p> * This returns the child element with the specified name. * An exception is thrown if there is more than one matching child or the child does not exist. * * @param childName the name to match * @return the child matching the name * @throws IllegalArgumentException if there is more than one match or no matches */ public XmlElement getChild(String childName) { return findChild(childName) .orElseThrow(() -> new IllegalArgumentException(Messages.format( "Unknown element '{}' in element '{}'", childName, name))); } /** * Finds the child element with the specified name, or empty if not found, * throwing an exception if more than one. * <p> * This returns the child element with the specified name. * If the element is not found, optional empty is returned. * * @param childName the name to match * @return the child matching the name, optional * @throws IllegalArgumentException if there is more than one match */ public Optional<XmlElement> findChild(String childName) { return streamChildren(childName).reduce(ensureOnlyOne()); } /** * Gets the child elements matching the specified name. * <p> * This returns all the child elements with the specified name. * * @param childName the name to match * @return the children matching the name */ public ImmutableList<XmlElement> getChildren(String childName) { return streamChildren(childName).collect(toImmutableList()); } /** * Gets the child elements matching the specified name. * <p> * This returns all the child elements with the specified name. * * @param childName the name to match * @return the children matching the name */ public Stream<XmlElement> streamChildren(String childName) { return children.stream().filter(child -> child.getName().equals(childName)); } //------------------------------------------------------------------------- @Override public MetaBean metaBean() { return META_BEAN; } @Override public <R> Property<R> property(String propertyName) { return metaBean().<R>metaProperty(propertyName).createProperty(this); } @Override public Set<String> propertyNames() { return metaBean().metaPropertyMap().keySet(); } //------------------------------------------------------------------------- /** * Checks if this element equals another. * <p> * This compares the entire state of the element, including all children. * * @param obj the other element, null returns false * @return true if equal */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof XmlElement) { XmlElement other = (XmlElement) obj; return name.equals(other.name) && Objects.equals(content, other.content) && attributes.equals(other.attributes) && children.equals(other.children); } return false; } /** * Returns a suitable hash code. * <p> * This includes the entire state of the element, including all children. * * @return the hash code */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + name.hashCode(); result = prime * result + content.hashCode(); result = prime * result + attributes.hashCode(); result = prime * result + children.hashCode(); return result; } /** * Returns a string summary of the element. * <p> * The string form includes the attributes and content, but summarizes the child elements. * * @return the string form */ @Override public String toString() { StringBuilder buf = new StringBuilder(512); buf.append('<').append(name); for (Entry<String, String> entry : attributes.entrySet()) { buf.append(' ').append(entry.getKey()).append('=').append('"').append(entry.getValue()).append('"'); } buf.append('>'); if (children.isEmpty()) { buf.append(content); } else { for (XmlElement child : children) { buf.append(System.lineSeparator()).append(" <").append(child.getName()).append(" ... />"); } buf.append(System.lineSeparator()); } buf.append("</").append(name).append('>'); return buf.toString(); } }