/* Copyright (c) 2012-2013 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.api;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.List;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.vividsolutions.jts.geom.Envelope;
/**
* The basic leaf element of a revision tree.
*/
public class NodeRef implements Bounded, Comparable<NodeRef> {
public static final String ROOT = "";
/**
* The character '/' used to separate paths (e.g. {@code path/to/node})
*/
public static final char PATH_SEPARATOR = '/';
/**
* Full path from the root tree to the object this ref points to
*/
private String parentPath;
/**
* The {@code Node} this object points to
*/
private Node node;
/**
* possibly {@link ObjectId#NULL NULL} id for the object describing the object this ref points
* to
*/
private ObjectId metadataId;
/**
* Constructs a new {@code Node} objects from a {@code Node} object without metadataId. It
* assumes that the passed {@code Node} does not have a metadataId value, and will not use it,
* even it it is present.
*
* @param node a Node representing the element this Node points to
* @param parentPath the path of the parent tree, may be an empty string
* @param metadataId the metadataId of the element
*/
public NodeRef(Node node, String parentPath, ObjectId metadataId) {
Preconditions.checkNotNull(node, "node is null");
Preconditions.checkNotNull(parentPath, "parentPath is null, did you mean an empty string?");
Preconditions.checkNotNull(metadataId, "metadataId is null, did you mean ObjectId.NULL?");
this.node = node;
this.parentPath = parentPath;
this.metadataId = metadataId;
}
/**
* Returns the parent path of the object this ref points to
*
* @return
*/
public String getParentPath() {
return parentPath;
}
/**
* Returns the {@code Node} this object points to
*
* @return the {@code Node} this object points to
*/
public Node getNode() {
return node;
}
/**
* Returns the full path from the root tree to the object this ref points to
* <p>
* This is a derived property, shortcut for
* <code>{@link #getParentPath()} + "/" + getNode().getName() </code>
*/
public String path() {
return NodeRef.appendChild(parentPath, node.getName());
}
/**
* @return the simple name of the {@link Node} this noderef points to
*/
public String name() {
return node.getName();
}
/**
* The id of the object this edge points to
*/
public ObjectId objectId() {
return node.getObjectId();
}
/**
* The node's metadata id, which can be given by the {@link Node#getMetadataId() node itself} or
* the metadata id given to this {@link NodeRef} constructor if the {@code Node} does not have a
* metadata id set, so that Nodes can inherit the metadata id from its parent tree.
*
* @return the node's metadata id if provided by {@link Node#getMetadataId()} or this node ref
* metadata id otherwise.
*/
public ObjectId getMetadataId() {
if (node.getMetadataId().isPresent() && !node.getMetadataId().get().isNull()) {
return node.getMetadataId().get();
} else {
return this.metadataId;
}
}
/**
* type of object this ref points to
*/
public RevObject.TYPE getType() {
return node.getType();
}
/**
* Tests equality over another {@code NodeRef} based on {@link #getParentPath() parent path},
* {@link #getNode() node} name and id, and {@link #getMetadataId()}
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof NodeRef)) {
return false;
}
NodeRef r = (NodeRef) o;
return parentPath.equals(r.parentPath) && node.equals(r.node)
&& getMetadataId().equals(r.getMetadataId());
}
/**
* Hash code is based on {@link #getParentPath() parent path}, {@link #getNode() node} name and
* id, and {@link #getMetadataId()}
*/
@Override
public int hashCode() {
return 17 ^ parentPath.hashCode() * node.getObjectId().hashCode()
* getMetadataId().hashCode();
}
/**
* Provides for natural ordering of {@code NodeRef}, based on {@link #path()}
*/
@Override
public int compareTo(NodeRef o) {
int c = parentPath.compareTo(o.getParentPath());
if (c == 0) {
return node.compareTo(o.getNode());
}
return c;
}
/**
* @return the Node represented as a readable string.
*/
@Override
public String toString() {
return new StringBuilder("NodeRef").append('[').append(path()).append(" -> ")
.append(node.getObjectId()).append(']').toString();
}
/**
* Returns the parent path of {@code fullPath}.
* <p>
* Given {@code fullPath == "path/to/node"} returns {@code "path/to"}, given {@code "node"}
* returns {@code ""}, given {@code null} returns {@code null}
*
* @param fullPath the full path to extract the parent path from
* @return non null parent path, empty string if {@code fullPath} has no children (i.e. no
* {@link #PATH_SEPARATOR}).
*/
public static @Nullable
String parentPath(@Nullable String fullPath) {
if (fullPath == null || fullPath.isEmpty()) {
return null;
}
int idx = fullPath.lastIndexOf(PATH_SEPARATOR);
if (idx == -1) {
return ROOT;
}
return fullPath.substring(0, idx);
}
/**
* Determines if the input path is valid.
*
* @param path
* @throws IllegalArgumentException
*/
public static void checkValidPath(final String path) {
if (path == null) {
throw new IllegalArgumentException("null path");
}
if (path.isEmpty()) {
throw new IllegalArgumentException("empty path");
}
if (path.charAt(path.length() - 1) == PATH_SEPARATOR) {
throw new IllegalArgumentException("path cannot end with path separator: " + path);
}
}
/**
* Returns the node of {@code fullPath}.
* <p>
* Given {@code fullPath == "path/to/node"} returns {@code "node" }, given {@code "node"}
* returns {@code "node"}, given {@code null} returns {@code null}
*
* @param fullPath the full path to extract the node from
* @return non null node, original string if {@code fullPath} has no path (i.e. no
* {@link #PATH_SEPARATOR}).
*/
public static @Nullable
String nodeFromPath(@Nullable String fullPath) {
if (fullPath == null || fullPath.isEmpty()) {
return null;
}
int idx = fullPath.lastIndexOf(PATH_SEPARATOR);
if (idx == -1) {
return fullPath;
}
return fullPath.substring(idx + 1, fullPath.length());
}
/**
* Determines if the given node path is a direct child of the parent path.
*
* @param parentPath
* @param nodePath
* @return true of {@code nodePath} is a direct child of {@code parentPath}, {@code false} if
* unrelated, sibling, same path, or nested child
*/
public static boolean isDirectChild(String parentPath, String nodePath) {
checkNotNull(parentPath, "parentPath");
checkNotNull(nodePath, "nodePath");
int idx = nodePath.lastIndexOf(PATH_SEPARATOR);
if (parentPath.isEmpty()) {
return !nodePath.isEmpty() && idx == -1;
}
return idx == parentPath.length() && nodePath.substring(0, idx).equals(parentPath);
}
/**
* Determines if the given node path is a child of the given parent path.
*
* @param parentPath
* @param nodePath
* @return true of {@code nodePath} is a child of {@code parentPath} at any depth level,
* {@code false} if unrelated, sibling, or same path
*/
public static boolean isChild(String parentPath, String nodePath) {
checkNotNull(parentPath, "parentPath");
checkNotNull(nodePath, "nodePath");
return nodePath.length() > parentPath.length()
&& (parentPath.isEmpty() || nodePath.charAt(parentPath.length()) == PATH_SEPARATOR)
&& nodePath.startsWith(parentPath);
}
/**
* Given {@code path == "path/to/node"} returns {@code ["path", "path/to", "path/to/node"]}
*
* @param path the path to analyze
* @return a sorted list of all paths that lead to the given path
*/
public static List<String> allPathsTo(final String path) {
checkNotNull(path);
checkArgument(!path.isEmpty());
StringBuilder sb = new StringBuilder();
List<String> paths = Lists.newArrayList();
final String[] steps = path.split("" + PATH_SEPARATOR);
int i = 0;
do {
sb.append(steps[i]);
paths.add(sb.toString());
sb.append(PATH_SEPARATOR);
i++;
} while (i < steps.length);
return paths;
}
/**
* Splits the given tree {@code path} into its node name components
*
* @param path non null, possibly empty path
* @return a list of path steps, or an empty list if the path is empty
*/
public static ImmutableList<String> split(final String path) {
checkNotNull(path);
if (path.isEmpty()) {
return ImmutableList.of();
}
final String[] steps = path.split("" + PATH_SEPARATOR);
return ImmutableList.copyOf(steps);
}
/**
* Constructs a new path by appending a child name to an existing parent path.
*
* @param parentTreePath full parent path
* @param childName name to append
*
* @return a new full path made by appending {@code childName} to {@code parentTreePath}
*/
public static String appendChild(String parentTreePath, String childName) {
checkNotNull(parentTreePath);
checkNotNull(childName);
return ROOT.equals(parentTreePath) ? childName : new StringBuilder(parentTreePath)
.append(PATH_SEPARATOR).append(childName).toString();
}
@Override
public boolean intersects(Envelope env) {
return node.intersects(env);
}
@Override
public void expand(Envelope env) {
node.expand(env);
}
/**
* @return the depth of the given path, being zero if the path is the root path (i.e. the empty
* string) or > 0 depending on how many steps compose the path
*/
public static int depth(String path) {
return split(path).size();
}
public static String removeParent(final String parentPath, final String childPath) {
checkArgument(isChild(parentPath, childPath));
ImmutableList<String> parent = split(parentPath);
ImmutableList<String> child = split(childPath);
child = child.subList(parent.size(), child.size());
String strippedChildPath = child.get(0);
for (int i = 1; i < child.size(); i++) {
appendChild(strippedChildPath, child.get(i));
}
return strippedChildPath;
}
}