ProjectAres/PGM/src/main/java/tc/oc/pgm/xml/Node.java

425 lines
14 KiB
Java

package tc.oc.pgm.xml;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Parent;
import org.jdom2.located.Located;
import tc.oc.commons.core.util.ArrayUtils;
import tc.oc.commons.core.util.Optionals;
/**
* A hybrid wrapper for either an {@link Element} or an {@link Attribute},
* enabling both of them to be handled in a generic way.
*/
public class Node {
private final Object node;
public Node(Element element) {
Preconditions.checkNotNull(element);
Preconditions.checkArgument(element instanceof BoundedElement);
this.node = element;
}
public Node(Attribute attribute) {
Preconditions.checkNotNull(attribute);
this.node = attribute;
}
public static Node of(Element element) {
return new Node(element);
}
public static Node of(Attribute attribute) {
return new Node(attribute);
}
private static boolean equals(Parent a, Parent b) {
if(a == b) return true;
if(a instanceof Element && b instanceof Element) {
return equals((Element) a, (Element) b);
}
return a.equals(b);
}
private static boolean equals(Element a, Element b) {
if(a == null || b == null) return false;
if(a == b) return true;
if(!equals(a.getParent(), b.getParent())) return false;
return a.getParent().indexOf(a) == b.getParent().indexOf(b);
}
private static boolean equals(Attribute a, Attribute b) {
if(a == null || b == null) return false;
if(a == b) return true;
if(!a.getName().equals(b.getName())) return false;
return equals(a.getParent(), b.getParent());
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(!(obj instanceof Node)) return false;
final Node that = (Node) obj;
if(this.node instanceof Attribute && that.node instanceof Attribute) {
return equals((Attribute) this.node, (Attribute) that.node);
} else if(this.node instanceof Element && that.node instanceof Element) {
return equals((Element) this.node, (Element) that.node);
}
return false;
}
@Override
public int hashCode() {
return node.hashCode();
}
public String getName() {
if(this.node instanceof Attribute) {
return ((Attribute) this.node).getName();
} else {
return ((Element) this.node).getName();
}
}
/**
* Gets the exact text content of the element or attribute
*/
public String getValue() {
if(this.node instanceof Attribute) {
return ((Attribute) this.node).getValue();
} else {
return ((Element) this.node).getText();
}
}
/**
* If this Node is wrapping an Attribute, returns the same as {@link #getValue()}.
* If this Node is wrapping an Element, returns {@link Element#getTextNormalize()}.
*/
public String getValueNormalize() {
if(this.node instanceof Attribute) {
return ((Attribute) this.node).getValue();
} else {
return ((Element) this.node).getTextNormalize();
}
}
public boolean isAttribute() {
return this.node instanceof Attribute;
}
public boolean isElement() {
return this.node instanceof Element;
}
public Attribute asAttribute() {
return asType(Attribute.class);
}
public Element asElement() {
return asType(Element.class);
}
public Stream<Node> attributes() {
return node instanceof Element
? ((Element) node).getAttributes().stream().map(Node::of)
: Stream.empty();
}
public Stream<Node> elements() {
return node instanceof Element
? ((Element) node).getChildren().stream().map(Node::of)
: Stream.empty();
}
public boolean hasNodes() {
if(!(node instanceof Element)) return false;
final Element element = (Element) node;
return !(element.getAttributes().isEmpty() &&
element.getChildren().isEmpty());
}
public Stream<Node> nodes() {
return Stream.concat(attributes(), elements());
}
private <T> T asType(Class<T> type) {
if(type.isInstance(node)) {
return type.cast(node);
}
throw new Error("Node is not a " + type.getSimpleName() + ": " + describeWithLocation());
}
public Optional<Attribute> tryAttribute() {
return Optionals.cast(node, Attribute.class);
}
public Optional<Element> tryElement() {
return Optionals.cast(node, Element.class);
}
public @Nullable Document getDocument() {
if(this.node instanceof Attribute) {
return ((Attribute) this.node).getDocument();
} else {
return ((Element) this.node).getDocument();
}
}
public Optional<Document> document() {
return Optional.ofNullable(getDocument());
}
public Optional<String> documentUri() {
return document().map(Document::getBaseURI);
}
public String describeType() {
if(node instanceof Element) return "element";
if(node instanceof Attribute) return "attribute";
return node.getClass().getSimpleName();
}
private static String describe(Element el) {
return "'" + el.getName() + "' element";
}
public String describe() {
if(node instanceof Element) {
return describe((Element) node);
} else {
Attribute attr = (Attribute) node;
return "'" + attr.getName() + "' attribute of " + describe(attr.getParent());
}
}
public static int startLine(Object node) {
if(node instanceof BoundedElement) {
return ((BoundedElement) node).getStartLine();
} else if(node instanceof Located) {
return ((Located) node).getLine();
} else if(node instanceof Attribute) {
return startLine(((Attribute) node).getParent());
} else {
return 0;
}
}
public int startLine() {
return startLine(node);
}
public int endLine() {
if(node instanceof BoundedElement) {
return ((BoundedElement) node).getEndLine();
} else if(node instanceof Located) {
return ((Located) node).getLine();
} else if(node instanceof Attribute) {
return startLine(((Attribute) node).getParent());
} else {
return 0;
}
}
public int column() {
if(node instanceof Located) {
return ((Located) node).getColumn();
} else {
return 0;
}
}
public static Optional<String> describeLocation(int startLine, int endLine, int column) {
if(startLine > 0) {
if(endLine > 0 && endLine != startLine) {
return Optional.of("line " + startLine + " to " + endLine);
}
if(column > 0) {
return Optional.of("line " + startLine + ", column " + column);
}
return Optional.of("line " + startLine);
}
return Optional.empty();
}
public Optional<String> describeLocation() {
return describeLocation(startLine(), endLine(), column());
}
public String describeWithLocation() {
return Optionals.reduce(describe(), describeLocation(), (d, l) -> d + " @ " + l);
}
public String describeWithDocumentAndLocation() {
return Optionals.reduce(describeWithLocation(), documentUri(), (loc, doc) -> doc + " - " + loc);
}
@Override
public String toString() {
return describeWithLocation();
}
private static Node wrapUnique(Node prev, boolean unique, String name, Object thing) throws InvalidXMLException {
if(thing == null) return prev;
Node node = thing instanceof Element ? new Node((Element) thing) : new Node((Attribute) thing);
if(unique && prev != null) throw new InvalidXMLException("Multiple values for '" + name + "'", node);
return node;
}
/**
* Return a new Node wrapping an Attribute of the given Element matching one of
* the given names, or null if the given Element has no matching Attributes.
*/
public static @Nullable Node fromAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return fromAttr(el, name, ImmutableSet.copyOf(aliases));
}
public static @Nullable Node fromAttr(Element el, String name, Set<String> aliases) throws InvalidXMLException {
Node node = null;
for(String alias : Sets.union(ImmutableSet.of(name), aliases)) {
node = wrapUnique(node, true, alias, el.getAttribute(alias));
}
return node;
}
public static Optional<Node> tryAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return Optional.ofNullable(fromAttr(el, name, aliases));
}
/**
* Return a new Node wrapping the named Attribute of the given Element.
* If the Attribute does not exist, throw an InvalidXMLException complaining about it.
*/
public static Node fromRequiredAttr(Element el, String name, String... aliases) throws InvalidXMLException {
Node node = fromAttr(el, name, aliases);
if(node == null) {
throw new InvalidXMLException("attribute '" + name + "' is required", el);
}
return node;
}
public static Stream<Node> attributes(Element el) {
return el.getAttributes().stream().map(Node::of);
}
public static Stream<Node> attributes(Element el, String name, String... aliases) {
return attributes(el, ImmutableSet.<String>builder().add(name).add(aliases).build());
}
public static Stream<Node> attributes(Element el, Set<String> names) {
return el.getAttributes()
.stream()
.filter(attr -> names.contains(attr.getName()))
.map(Node::of);
}
public static Stream<Node> elements(Element parent) {
return parent.getChildren().stream().map(Node::of);
}
public static Stream<Node> elements(Element parent, String name, String... aliases) {
return elements(parent, ImmutableSet.<String>builder().add(name).add(aliases).build());
}
public static Stream<Node> elements(Element parent, Set<String> names) {
return parent.getChildren()
.stream()
.filter(el -> names.contains(el.getName()))
.map(Node::of);
}
public static Stream<Node> nodes(Element parent) {
return Stream.concat(attributes(parent), elements(parent));
}
public static Stream<Node> nodes(Node parent) {
return parent.isElement() ? nodes(parent.asElement()) : Stream.empty();
}
public static Stream<Node> nodes(Element parent, Set<String> names) {
return Stream.concat(attributes(parent, names), elements(parent, names));
}
public static List<Node> fromAttrs(Element el) throws InvalidXMLException {
return Lists.transform(el.getAttributes(), Node::of);
}
public static List<Node> fromChildren(List<Node> nodes, Element el, String name, String... aliases) throws InvalidXMLException {
aliases = ArrayUtils.append(aliases, name);
for(Element child : el.getChildren()) {
if(ArrayUtils.contains(aliases, child.getName())) {
nodes.add(Node.of(child));
}
}
return nodes;
}
public static List<Node> fromChildren(Element el, String name, String... aliases) throws InvalidXMLException {
return fromChildren(new ArrayList<>(), el, name, aliases);
}
public static @Nullable Node fromNullable(Element el) {
return el == null ? null : Node.of(el);
}
public static @Nullable Node fromNullable(Attribute attr) {
return attr == null ? null : Node.of(attr);
}
public static @Nullable Node fromChildOrAttr(Element el, boolean unique, boolean required, String name, String... aliases) throws InvalidXMLException {
aliases = ArrayUtils.append(aliases, name);
Node node = null;
for(String alias : aliases) {
node = wrapUnique(node, unique, alias, el.getAttribute(alias));
for(Element child : el.getChildren(alias)) {
node = wrapUnique(node, unique, alias, child);
}
}
if(required && node == null) {
throw new InvalidXMLException("attribute or child element '" + name + "' is required", el);
}
return node;
}
public static @Nullable Node fromChildOrAttr(Element el, boolean unique, String name, String... aliases) throws InvalidXMLException {
return fromChildOrAttr(el, unique, false, name, aliases);
}
public static @Nullable Node fromChildOrAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return fromChildOrAttr(el, true, name, aliases);
}
public static @Nullable Node fromLastChildOrAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return fromChildOrAttr(el, false, false, name, aliases);
}
public static Node fromRequiredChildOrAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return fromChildOrAttr(el, true, true, name, aliases);
}
public static Node fromRequiredLastChildOrAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return fromChildOrAttr(el, false, true, name, aliases);
}
public static Optional<Node> childOrAttr(Element el, String name, String... aliases) throws InvalidXMLException {
return Optional.ofNullable(fromChildOrAttr(el, name, aliases));
}
}