package ...;
import lombok.*;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import static java.util.Objects.*;
/**
* Models a general path made of nodes to which we can optionally attach values and URLs.
* <p/>
* Note that while {@link Node path node} instances {@link Node#setValue(String) are} {@link Node#setRefUri(String)
* mutable} and do not define an identity, we can easily imagine a {@link #getFullPath() full path} identity.
*
* @param <SELF> the actual path (sub)type in order to leverage a fluent style API
* @author <a href="mailto:octavian.nita@gmail.com">Octavian Nita</a>
* @version 1.0, 2019/05/05
* @see <a href="https://stackoverflow.com/a/23895571/272939">Answer to <em>Fluent API with inheritance and generics</em></a>
* @see <a href="https://stackoverflow.com/a/7355094/272939">Answer to <em>Is there a way to refer to the current type
* with a type variable?</em></a>
*/
@Getter
@ToString
public class Path<SELF extends Path<SELF>> {
/**
* @see <a href="https://stackoverflow.com/a/23895571/272939">Answer to <em>Fluent API with inheritance and
* generics</em></a>
* @see <a href="https://stackoverflow.com/a/7355094/272939">Is there a way to refer to the current type with a type
*/
@SuppressWarnings("unchecked")
protected final SELF self() {
return (SELF) this;
}
/**
* Eventual prefix of the path specified by the added nodes. Can be {@code null}.
*/
private final String parentPath;
/**
* Path nodes that come after the {@link #parentPath}, if the latter is not {@code null}.
*/
private final List<Node> pathNodes = new LinkedList<>();
/**
* Path (node) separator.
*/
protected static final String separator = "/";
/**
* Path (node) separator as (escaped) regular expression.
*/
protected static final String separatorRegex = "\\Q" + separator + "\\E";
@NonNull
protected List<Node> getPathNodes() { return new LinkedList<>(pathNodes); }
public Path(String parentPath) {
this.parentPath = parentPath != null && parentPath.endsWith(separator)
? parentPath .substring(0, parentPath.length() - separator.length())
: parentPath;
}
public Path() {
this(null);
}
public SELF add(@NonNull String nodeName) {
pathNodes.add(new Node(nodeName));
return self();
}
public SELF add(@NonNull String nodeName, String value) {
pathNodes.add(new Node(nodeName, value));
return self();
}
public SELF add(@NonNull String nodeName, String value, String refUri) {
pathNodes.add(new Node(nodeName, value, refUri));
return self();
}
/**
* @return the full path of the path (usually a convenience) or the empty string if no parent path and no nodes
* are defined
*/
@NonNull
public String getFullPath() {
final StringBuilder fullPath = parentPath == null ? new StringBuilder() : new StringBuilder(parentPath);
for (Node currentNode : pathNodes) {
if (fullPath.length() > 0) {
fullPath.append(separator);
}
fullPath.append(currentNode.getName());
}
return fullPath.toString();
}
/**
* Invokes {@code action} for each path node, passing in the node as well as the current path to it.
*
* @return the full path of the path (usually a convenience) or the empty string if no parent path and no nodes
* are defined
*/
@NonNull
public String forEachPathNode(BiConsumer<? super String, ? super Node> action) {
requireNonNull(action);
if (pathNodes.isEmpty()) {
return "";
}
final StringBuilder currentPath = parentPath == null ? new StringBuilder() : new StringBuilder(parentPath);
for (Node currentNode : pathNodes) {
if (currentPath.length() > 0) {
currentPath.append(separator);
}
currentPath.append(currentNode.getName());
action.accept(currentPath.toString(), currentNode);
}
return currentPath.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Path)) {
return false;
}
final Path<?> path = (Path<?>) o;
if (!Objects.equals(parentPath, path.parentPath) || pathNodes.size() != path.pathNodes.size()) {
return false;
}
final int nodeCount = pathNodes.size();
for (int i = 0; i < nodeCount; i++) {
if (!pathNodes.get(i).getName().equals(path.pathNodes.get(0).getName())) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int hash = hash(parentPath);
for (Node pathNode : pathNodes) {
hash = hash(hash, pathNode.getName());
}
return hash;
}
@Getter
@Setter
@ToString
public static class Node {
@NonNull
private final String name;
private String value;
private String refUri;
/**
* By default, the {@link #getValue() value} is also set to {@code name} (although un-{@link
* #sanitizeAsName(String)
* sanitized}).
*/
protected Node(@NonNull String name) { this(name, name); }
protected Node(@NonNull String name, String value) {
this.name = sanitizeAsName(name);
this.value = value;
}
protected Node(@NonNull String name, String value, String refUri) {
this(name, value);
this.refUri = refUri;
}
public static String sanitizeAsName(String name) {
return name == null ? null : INVALID_NAME_CHARS.matcher(name).replaceAll("_");
}
private static final Pattern INVALID_NAME_CHARS = Pattern.compile("[^a-zA-Z0-9/_-]+");
}
}