cdc.io.data.Parent Maven / Gradle / Ivy
package cdc.io.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import cdc.io.data.paths.Part;
import cdc.io.data.paths.Path;
import cdc.util.function.IterableUtils;
import cdc.util.function.Predicates;
import cdc.util.lang.Checks;
import cdc.util.lang.Operators;
/**
* Interface implemented by nodes that are parents (they have children nodes): Document and Element.
*
* @author Damien Carbonne
*
*/
public interface Parent extends Node {
@Override
public Parent clone(boolean recurse);
/**
* Returns {@code true} when a child can be added.
*
* @param child The child
* @return {@code true} when {@code child} can be added.
*/
public boolean canAddChild(Child child);
public List getModifiableChildren();
/**
* @return A list of children.
*/
public List extends Child> getChildren();
/**
* Returns an Iterable of children that are instance of a given class.
*
* @param Type of searched children.
* @param childClass The class of searched children.
* @return An Iterable of children that are instance of {@code childClass}.
*/
public default Iterable getChildren(Class childClass) {
return IterableUtils.filterAndConvert(childClass,
getChildren(),
Predicates.isInstanceOf(childClass));
}
/**
* Returns an Iterable of children that are instance of a given class and match a predicate.
*
* @param Type of searched children.
* @param childClass The class of searched children.
* @param predicate The predicate.
* @return An Iterable of children that are instance of {@code childClass} and match {@code predicate}.
*/
public default Iterable getChildren(Class childClass,
Predicate super T> predicate) {
return IterableUtils.filterAndConvert(childClass,
getChildren(),
Predicates.isInstanceOf(childClass).and(predicate));
}
public default T getChild(Class childClass,
Predicate super T> predicate) {
return IterableUtils.getFirstOrNull(getChildren(childClass, predicate));
}
public default T getChild(Class childClass) {
return getChild(childClass, Predicates.alwaysTrue());
}
/**
* Returns the child that is an instance of a given class, matches a Predicate and is at a given position.
*
* @param Type of searched children.
* @param childClass The class of searched children.
* @param predicate The predicate.
* @param index The index of the searched child.
* This index takes into account elements that match {@code childClass} and {@code predicate}.
* @return The element matching {@code childClass} and {@code predicate} and at {@code index}, or {@code null}.
*/
public default T getChildAt(Class childClass,
Predicate super T> predicate,
int index) {
return IterableUtils.getAtOrNull(getChildren(childClass, predicate), index);
}
/**
* Returns the child that is an instance of a given class and is at a given position.
*
* @param Type of searched children.
* @param childClass The class of searched children.
* @param index The index of the searched child.
* This index takes into account elements that match {@code childClass}.
* @return The element matching {@code childClass} and at {@code index}, or {@code null}.
*/
public default T getChildAt(Class childClass,
int index) {
return IterableUtils.getAtOrNull(getChildren(childClass), index);
}
/**
* @return An Iterable of children elements.
*/
public default Iterable getElements() {
return getChildren(Element.class);
}
/**
* @return {@code true} if this parent has children elements.
*/
public default boolean hasElements() {
return !IterableUtils.isEmpty(getElements());
}
/**
* Returns an Iterable of children elements that have a given name.
*
* @param name The name.
* @return An Iterable containing elements named {@code name}.
*/
public default Iterable getElementsNamed(String name) {
return getChildren(Element.class, Element.named(name));
}
/**
* Returns the child element that has a given name and index.
*
* @param name The name.
* @param index The index of the searched child element.
* This index takes into account children that are elements and are named {@code name}.
* @return The corresponding element or {@code null}.
*/
public default Element getElementNamedAt(String name,
int index) {
return IterableUtils.getAtOrNull(getElementsNamed(name), index);
}
/**
* Returns the first child element that has a given name or {@code null}.
*
* @param name The name.
* @return The first child element named {@code name} or {@code null}.
*/
public default Element getElementNamed(String name) {
return getElementNamedAt(name, 0);
}
/**
* Returns the sub-child at a given path.
*
* @param names The names of elements.
* @return The element that has a path.
*/
public default Element getElement(String... names) {
Checks.isNotNullOrEmpty(names, "names");
Parent parent = this;
for (final String name : names) {
if (parent != null) {
parent = parent.getElementNamed(name);
}
}
return (Element) parent;
}
/**
* Returns the relative element with a given path.
*
* @param path The relative path of the searched element.
* @return The element at {code path} or {@code null}.
*/
public default Element getElement(Path path) {
Parent parent = this;
for (final Part part : path.getParts()) {
switch (part.getType()) {
case DOT:
// Ignore
break;
case DOT_DOT:
if (parent instanceof Child) {
parent = ((Child) parent).getParent();
} else {
parent = null;
}
break;
case ELEMENT:
if (parent != null) {
parent = parent.getElementNamed(part.getElementName());
}
break;
case ATTRIBUTE:
parent = null;
break;
case SELECTOR:
if (parent != null) {
parent = parent.getChild(Element.class,
Element.namedWithAttribute(part.getElementName(),
part.getSelectorName(),
part.getSelectorValue()));
}
break;
}
}
if (parent instanceof Element) {
return (Element) parent;
} else {
return null;
}
}
/**
* Returns an Iterable of children elements that have a given name, ignoring case.
*
* @param name The name.
* @return An Iterable containing elements named {@code name} ignoring case.
*/
public default Iterable getElementsNamedIgnoreCase(String name) {
return getChildren(Element.class, Element.namedIgnoreCase(name));
}
/**
* Returns the child element that has a given name, ignoring case, and index.
*
* @param name The name.
* @param index The index of the searched child element.
* This index takes into account children that are elements and are named {@code name} ignoring case.
* @return The corresponding element or {@code null}.
*/
public default Element getElementNamedIgnoreCaseAt(String name,
int index) {
return IterableUtils.getAtOrNull(getElementsNamedIgnoreCase(name), index);
}
/**
* Returns the first child element that has a given name, ignoring case, or {@code null}.
*
* @param name The name.
* @return The first child element named {@code name}, ignoring case, or {@code null}.
*/
public default Element getElementNamedIgnoreCase(String name) {
return getElementNamedIgnoreCaseAt(name, 0);
}
public default Element getElementNamedWithAttribute(String name,
String attributeName,
String attributeValue) {
return getChild(Element.class, Element.namedWithAttribute(name, attributeName, attributeValue));
}
/**
* @return An Iterable if text children.
*/
public default Iterable getTexts() {
return getChildren(Text.class);
}
/**
* @return {@code true} if this parent has texts children.
*/
public default boolean hasTexts() {
return !IterableUtils.isEmpty(getTexts());
}
/**
* @return An Iterable if comment children.
*/
public default Iterable getComments() {
return getChildren(Comment.class);
}
/**
* @return {@code true} if this parent has comments children.
*/
public default boolean hasComments() {
return !IterableUtils.isEmpty(getComments());
}
/**
* Returns the number of children that match a class and predicate.
*
* @param Type of counted children.
* @param childClass The class of counted children.
* @param predicate The predicate.
* @return The number of children that match {@code childClass} and {@code predicate}.
*/
public default int getChildrenCount(Class childClass,
Predicate super T> predicate) {
return IterableUtils.size(getChildren(childClass, predicate));
}
/**
* Returns the number of children that match a class.
*
* @param childClass The class of counted children.
* @return The number of children that match {@code childClass}.
*/
public default int getChildrenCount(Class extends Node> childClass) {
return IterableUtils.size(getChildren(childClass));
}
/**
* @return The number of children.
*/
public default int getChildrenCount() {
return getChildren().size();
}
/**
* Returns {@code true} if this parent has children of a given class and matching a predicate.
*
* @param Type of searched children.
*
* @param childClass The child class.
* @param predicate The predicate.
* @return {@code true} if this parent has children of {@code childClass} class matching {@code predicate}.
*/
public default boolean hasChildren(Class childClass,
Predicate super T> predicate) {
return !IterableUtils.isEmpty(getChildren(childClass, predicate));
}
/**
* Returns {@code true} if this parent has children of a given class.
*
* @param childClass The child class.
* @return {@code true} if this parent has children of {@code childClass} class.
*/
public default boolean hasChildren(Class extends Node> childClass) {
return !IterableUtils.isEmpty(getChildren(childClass));
}
/**
* @return {@code true} if this parent has children.
*/
public default boolean hasChildren() {
return !IterableUtils.isEmpty(getChildren());
}
/**
* Returns the child at a given, index or null.
*
* {@code null} is returned when index is invalid.
*
* @param index The index.
* @return The child at {@code index}.
* @throws IndexOutOfBoundsException When index is out of range ({@code index < 0 || index >= getChildrenCount()}).
*/
public default Child getChildAt(int index) {
return getChildren().get(index);
}
/**
* @return The last child, if any, or {@code null}.
*/
public default Child getLastChild() {
final List extends Child> children = getChildren();
if (children.isEmpty()) {
return null;
} else {
return children.get(children.size() - 1);
}
}
/**
* Adds a child to this parent.
*
* If possible, child current parent is changed to this node.
*
* @param Type of the child.
* @param child The child. MUST NOT be null.
* @return The input {@code child}.
* @throws IllegalArgumentException If {@code child} is {@code null}.
* @throws IllegalStateException If {@code child}'s parent can not be set.
*/
public default C addChild(C child) {
Checks.isNotNull(child, "child");
// setParent calls canAddChild()
child.setParent(this);
return child;
}
/**
* Creates and adds a child element as last child, if possible.
*
* @param name The child name.
* @return the created element.
* @throws IllegalStateException If {@code child}'s parent can not be set.
*/
public default Element addElement(String name) {
final Element child = new Element(name);
return addChild(child);
}
/**
* Adds a comment as last child.
*
* @param content The comment content.
* @param merge If {@code true}, then if last child exists and is a comment,
* {@code content} is added to this last child.
* Otherwise, a comment child is created and added to last position.
* @return The modified or created comment.
*/
public default Comment addComment(String content,
boolean merge) {
if (merge) {
final Child last = getLastChild();
if (last != null && last.getType() == NodeType.COMMENT) {
((Comment) last).appendContent(content);
return (Comment) last;
}
}
final Comment result = new Comment(content);
addChild(result);
return result;
}
/**
* Adds a comment as last child.
*
* If last child exists and is a comment, {@code content} is added to this last child.
* Otherwise, a comment child is created and added to last position.
*
* @param content The comment content.
* @return The modified or created comment.
*/
public default Comment addComment(String content) {
return addComment(content, true);
}
/**
* Adds all children of an Iterable to this parent.
*
* @param children The children.
*/
public default void addChildren(Iterable extends Child> children) {
for (final Child child : children) {
addChild(child);
}
}
/**
* Adds all children of an array to this parent.
*
* @param children The children.
*/
public default void addChildren(Child... children) {
for (final Child child : children) {
addChild(child);
}
}
/**
* Remove a child.
*
* If child is not a child of this parent, then false is returned.
*
* @param child The child.
* @return {@code true} if child was removed, {@code false} otherwise.
*/
public default boolean removeChild(Child child) {
if (child != null && child.getParent() == this) {
getModifiableChildren().remove(child);
child.setParent(null);
return true;
} else {
return false;
}
}
/**
* Removes the child located at a given index.
*
* If index is invalid, returns {@code false}.
*
* @param index The index (0-based).
* @return The removed child.
* @throws IndexOutOfBoundsException When index is out of range ({@code index < 0 || index >= getChildrenCount()}).
*/
public default Child removeChildAt(int index) {
final AbstractChild child = (AbstractChild) getModifiableChildren().remove(index);
child.resetParent();
return child;
}
/**
* Removes all children locally.
*/
public default void removeChildren() {
while (hasChildren()) {
removeChildAt(getChildrenCount() - 1);
}
}
/**
* Removes all children that match a predicate.
*
* This does only a shallow removal.
* For deep removal, use {@link cdc.io.data.util.DataUtils}.
*
* @param predicate The predicate.
*/
public default void removeChildren(Predicate super Child> predicate) {
List indices = null;
// Add candidates to removal in reverse order
// Better performances for removal
for (int index = getChildrenCount() - 1; index >= 0; index--) {
final Child child = getChildAt(index);
if (predicate.test(child)) {
if (indices == null) {
indices = new ArrayList<>();
}
indices.add(index);
}
}
if (indices != null) {
for (final int index : indices) {
final Child removed = removeChildAt(index);
assert removed != null;
}
}
}
/**
* Removes all children that match a predicate, recursively.
*
* @param predicate The predicate.
* @param pre If {@code true}, removal is applied before recursion, after otherwise.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void removeChildren(Predicate super Child> predicate,
boolean pre,
boolean recurse) {
if (pre) {
removeChildren(predicate);
}
if (recurse) {
for (final Element child : getElements()) {
child.removeChildren(predicate, pre, true);
}
}
if (!pre) {
removeChildren(predicate);
}
}
/**
* Removes all children elements that have a given name.
*
* @param name The name of elements to remove.
*/
public default void removeElementsNamed(String name) {
removeChildren(Element.named(name));
}
/**
* Removes all children elements that have a given name, recursively.
*
* @param name The name of elements to remove.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void removeElementsNamed(String name,
boolean recurse) {
removeChildren(Element.named(name), true, recurse);
}
/**
* Removes all comments locally.
*/
public default void removeComments() {
removeChildren(IS_COMMENT);
}
/**
* Removes all comments recursively.
*
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void removeComments(boolean recurse) {
removeChildren(IS_COMMENT, true, recurse);
}
/**
* Removes all texts locally.
*/
public default void removeTexts() {
removeChildren(IS_TEXT);
}
/**
* Removes all texts recursively.
*
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void removeTexts(boolean recurse) {
removeChildren(IS_TEXT, true, recurse);
}
/**
* Removes all ignorable texts locally.
*/
public default void removeIgnorableTexts() {
removeChildren(IS_IGNORABLE_TEXT);
}
/**
* Removes all ignorable texts recursively.
*
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void removeIgnorableTexts(boolean recurse) {
removeChildren(IS_IGNORABLE_TEXT, true, recurse);
}
/**
* Sorts children locally.
*
* @param comparator The comparator.
*/
public default void sortChildren(Comparator super Child> comparator) {
if (hasChildren()) {
Collections.sort(getChildren(), comparator);
}
}
/**
* Sorts children recursively.
*
* @param comparator The comparator.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void sortChildren(Comparator super Child> comparator,
boolean recurse) {
sortChildren(comparator);
if (recurse) {
for (final Element child : getElements()) {
child.sortChildren(comparator, true);
}
}
}
/**
* Merges all consecutive texts locally.
*/
public default void mergeTexts() {
if (getChildrenCount() > 1) {
Child ref = getChildAt(0);
int index = 1;
while (index < getChildrenCount()) {
final Child next = getChildAt(index);
if (ref.getType() == NodeType.TEXT
&& next.getType() == NodeType.TEXT
&& ((Text) ref).getKind() == ((Text) next).getKind()) {
// ref and next are both texts: merge them
// They have the same cdata attribute
((Text) ref).appendContent(((Text) next).getContent());
// Remove next
next.detach();
// Do not change index and ref
} else {
ref = next;
index++;
}
}
}
}
/**
* Merges all consecutive texts recursively.
*
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void mergeTexts(boolean recurse) {
mergeTexts();
if (recurse) {
for (final Element child : getElements()) {
child.mergeTexts(true);
}
}
}
/**
* Locally sets TextKind of all texts.
*
* @param kind The text kind.
*/
public default void setTextsKind(TextKind kind) {
for (final Text child : getTexts()) {
child.setKind(kind);
}
}
/**
* Recursively sets TextKind of all texts.
*
* @param kind The text kind.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void setTextsKind(TextKind kind,
boolean recurse) {
setTextsKind(kind);
if (recurse) {
for (final Element child : getElements()) {
child.setTextsKind(kind, true);
}
}
}
/**
* Merges all consecutive comments locally.
*/
public default void mergeComments() {
if (getChildrenCount() > 1) {
Child ref = getChildAt(0);
int index = 1;
while (index < getChildrenCount()) {
final Child next = getChildAt(index);
if (ref.getType() == NodeType.COMMENT && next.getType() == NodeType.COMMENT) {
// ref and next are both texts: merge them
((Comment) ref).appendContent(((Comment) next).getContent());
// Remove next
next.detach();
// Do not change index and ref
} else {
ref = next;
index++;
}
}
}
}
/**
* Merges all consecutive comments recursively.
*
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void mergeComments(boolean recurse) {
mergeComments();
if (recurse) {
for (final Element child : getElements()) {
child.mergeComments(true);
}
}
}
/**
* Changes all texts locally.
*
* @param modifier A function that take content and returns new content.
*/
public default void changeTexts(UnaryOperator modifier) {
for (final Text child : getTexts()) {
final String content = child.getContent();
final String newContent = modifier.apply(content);
if (Operators.notEquals(content, newContent)) {
child.setContent(newContent);
}
}
}
/**
* Changes all texts recursively.
*
* @param modifier A function that take content and returns new content.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void changeTexts(UnaryOperator modifier,
boolean recurse) {
changeTexts(modifier);
if (recurse) {
for (final Element child : getElements()) {
child.changeTexts(modifier, true);
}
}
}
/**
* Changes or set text under element that have a given name, locally.
*
* This will work on elements that are empty or have text and comment children.
* It will ignore elements that have children elements.
*
* Children nodes are preserved if possible.
* Otherwise, they are all removed and replace if necessary.
* WARNING: This may remove comments.
*
* @param name The element name.
* @param modifier A function that take content and returns new content.
*/
public default void changeNamedTexts(String name,
UnaryOperator modifier) {
for (final Element child : getElementsNamed(name)) {
switch (child.getContentType()) {
case EMPTY:
case TEXT:
final String content = child.getText();
final String newContent = modifier.apply(content);
if (Operators.notEquals(content, newContent)) {
if (child.getChildrenCount(Text.class) == 1 && newContent != null && !newContent.isEmpty()) {
final Text text = child.getChild(Text.class);
text.clearContent();
text.appendContent(newContent);
} else {
child.removeChildren();
if (newContent != null && !newContent.isEmpty()) {
child.addText(modifier.apply(content));
}
}
}
break;
default:
break;
}
}
}
/**
* Changes or set text under element that have a given name, recursively.
*
* @param name The element name.
* @param modifier A function that take content and returns new content.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void changeNamedTexts(String name,
UnaryOperator modifier,
boolean recurse) {
changeNamedTexts(name, modifier);
if (recurse) {
for (final Element child : getElements()) {
child.changeNamedTexts(name, modifier, true);
}
}
}
/**
* Changes all comments locally.
*
* @param modifier A function that take content and returns new content.
*/
public default void changeComments(UnaryOperator modifier) {
for (final Comment child : getComments()) {
final String content = child.getContent();
final String newContent = modifier.apply(content);
if (Operators.notEquals(content, newContent)) {
child.setContent(newContent);
}
}
}
/**
* Changes all comments recursively.
*
* @param modifier A function that take content and returns new content.
* @param recurse If {@code true}, then this is applied recursively.
*/
public default void changeComments(UnaryOperator modifier,
boolean recurse) {
changeComments(modifier);
if (recurse) {
for (final Element child : getElements()) {
child.changeComments(modifier, true);
}
}
}
}