Last active
May 6, 2019 14:47
-
-
Save octavian-nita/f31d49617e9534a5722749059b3fc197 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:[email protected]">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/_-]+"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment