diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixNode.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixNode.java new file mode 100644 index 00000000000..3009c9a4e8c --- /dev/null +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixNode.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.ozone.util; + +import java.util.HashMap; + +/** + * Wrapper class for Radix tree node representing Ozone prefix path segment + * separated by "/". + */ +public class RadixNode { + + public RadixNode(String name) { + this.name = name; + this.children = new HashMap<>(); + } + + public String getName() { + return name; + } + + public boolean hasChildren() { + return children.isEmpty(); + } + + public HashMap getChildren() { + return children; + } + + public void setValue(T v) { + this.value = v; + } + + public T getValue() { + return value; + } + + private HashMap children; + + private String name; + + // TODO: k/v pairs for more metadata as needed + private T value; +} diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixTree.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixTree.java new file mode 100644 index 00000000000..72e9ab3f5e7 --- /dev/null +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/util/RadixTree.java @@ -0,0 +1,214 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.ozone.util; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.hadoop.ozone.OzoneConsts; + +import java.util.ArrayList; +import java.util.HashMap; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Wrapper class for handling Ozone prefix path lookup of ACL APIs + * with radix tree. + */ +public class RadixTree { + + /** + * create a empty radix tree with root only. + */ + public RadixTree() { + root = new RadixNode(PATH_DELIMITER); + } + + /** + * If the Radix tree contains root only. + * @return true if the radix tree contains root only. + */ + public boolean isEmpty() { + return root.hasChildren(); + } + + /** + * Insert prefix tree node without value, value can be ACL or other metadata + * of the prefix path. + * @param path + */ + public void insert(String path) { + insert(path, null); + } + + /** + * Insert prefix tree node with value, value can be ACL or other metadata + * of the prefix path. + * @param path + * @param val + */ + public void insert(String path, T val) { + // all prefix path inserted should end with "/" + RadixNode n = root; + Path p = Paths.get(path); + for (int level = 0; level < p.getNameCount(); level++) { + HashMap child = n.getChildren(); + String component = p.getName(level).toString(); + if (child.containsKey(component)) { + n = child.get(component); + } else { + RadixNode tmp = new RadixNode(component); + child.put(component, tmp); + n = tmp; + } + } + if (val != null) { + n.setValue(val); + } + } + + /** + * Get the last node in the exact prefix path that matches in the tree. + * @param path - prefix path + * @return last node in the prefix tree or null if non exact prefix matchl + */ + public RadixNode getLastNodeInPrefixPath(String path) { + List> lpp = getLongestPrefixPath(path); + Path p = Paths.get(path); + if (lpp.size() != p.getNameCount() + 1) { + return null; + } else { + return lpp.get(p.getNameCount()); + } + } + + /** + * Remove prefix path. + * @param path + */ + public void removePrefixPath(String path) { + Path p = Paths.get(path); + removePrefixPathInternal(root, p, 0); + } + + /** + * Recursively remove non-overlapped part of the prefix path from radix tree. + * @param current current radix tree node. + * @param path prefix path to be removed. + * @param level current recursive level. + * @return true if current radix node can be removed. + * (not overlapped with other path), + * false otherwise. + */ + private boolean removePrefixPathInternal(RadixNode current, + Path path, int level) { + // last component is processed + if (level == path.getNameCount()) { + return current.hasChildren(); + } + + // not last component, recur for next component + String name = path.getName(level).toString(); + RadixNode node = current.getChildren().get(name); + if (node == null) { + return false; + } + + if (removePrefixPathInternal(node, path, level+1)) { + current.getChildren().remove(name); + return current.hasChildren(); + } + return false; + } + + /** + * Get the longest prefix path. + * @param path - prefix path. + * @return longest prefix path as list of RadixNode. + */ + public List> getLongestPrefixPath(String path) { + RadixNode n = root; + Path p = Paths.get(path); + int level = 0; + List> result = new ArrayList<>(); + result.add(root); + while (level < p.getNameCount()) { + HashMap children = n.getChildren(); + if (children.isEmpty()) { + break; + } + String component = p.getName(level).toString(); + if (children.containsKey(component)) { + n = children.get(component); + result.add(n); + level++; + } else { + break; + } + } + return result; + } + + @VisibleForTesting + /** + * Convert radix path to string format for output. + * @param path - radix path represented by list of radix nodes. + * @return radix path as string separated by "/". + * Note: the path will always be normalized with and ending "/". + */ + public static String radixPathToString(List> path) { + StringBuilder sb = new StringBuilder(); + for (RadixNode n : path) { + sb.append(n.getName()); + sb.append(n.getName().equals(PATH_DELIMITER) ? "" : PATH_DELIMITER); + } + return sb.toString(); + } + + /** + * Get the longest prefix path. + * @param path - prefix path. + * @return longest prefix path as String separated by "/". + */ + public String getLongestPrefix(String path) { + RadixNode n = root; + Path p = Paths.get(path); + int level = 0; + while (level < p.getNameCount()) { + HashMap children = n.getChildren(); + if (children.isEmpty()) { + break; + } + String component = p.getName(level).toString(); + if (children.containsKey(component)) { + n = children.get(component); + level++; + } else { + break; + } + } + return level >= 1 ? + Paths.get(root.getName()).resolve(p.subpath(0, level)).toString() : + root.getName(); + } + + // root of a radix tree has a name of "/" and may optionally has it value. + private RadixNode root; + + private final static String PATH_DELIMITER = OzoneConsts.OZONE_URI_DELIMITER; +} diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/TestRadixTree.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/TestRadixTree.java new file mode 100644 index 00000000000..ceed5346f81 --- /dev/null +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/TestRadixTree.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.util; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test Ozone Radix tree operations. + */ +public class TestRadixTree { + + final static RadixTree ROOT = new RadixTree<>(); + + @BeforeClass + public static void setupRadixTree() { + // Test prefix paths with an empty tree + assertEquals(true, ROOT.isEmpty()); + assertEquals("/", ROOT.getLongestPrefix("/a/b/c")); + assertEquals("/", RadixTree.radixPathToString( + ROOT.getLongestPrefixPath("/a/g"))); + // Build Radix tree below for testing. + // a + // | + // b + // / \ + // c e + // / \ / \ \ + // d f g dir1 dir2(1000) + // | + // g + // | + // h + ROOT.insert("/a/b/c/d"); + ROOT.insert("/a/b/c/d/g/h"); + ROOT.insert("/a/b/c/f"); + ROOT.insert("/a/b/e/g"); + ROOT.insert("/a/b/e/dir1"); + ROOT.insert("/a/b/e/dir2", 1000); + } + + /** + * Tests if insert and build prefix tree is correct. + */ + @Test + public void testGetLongestPrefix() { + assertEquals("/a/b/c", ROOT.getLongestPrefix("/a/b/c")); + assertEquals("/a/b", ROOT.getLongestPrefix("/a/b")); + assertEquals("/a", ROOT.getLongestPrefix("/a")); + assertEquals("/a/b/e/g", ROOT.getLongestPrefix("/a/b/e/g/h")); + + assertEquals("/", ROOT.getLongestPrefix("/d/b/c")); + assertEquals("/a/b/e", ROOT.getLongestPrefix("/a/b/e/dir3")); + assertEquals("/a/b/c/d", ROOT.getLongestPrefix("/a/b/c/d/p")); + + assertEquals("/a/b/c/f", ROOT.getLongestPrefix("/a/b/c/f/p")); + } + + @Test + public void testGetLongestPrefixPath() { + List> lpp = + ROOT.getLongestPrefixPath("/a/b/c/d/g/p"); + RadixNode lpn = lpp.get(lpp.size()-1); + assertEquals("g", lpn.getName()); + lpn.setValue(100); + + + List> lpq = + ROOT.getLongestPrefixPath("/a/b/c/d/g/q"); + RadixNode lqn = lpp.get(lpq.size()-1); + System.out.print(RadixTree.radixPathToString(lpq)); + assertEquals(lpn, lqn); + assertEquals("g", lqn.getName()); + assertEquals(100, (int)lqn.getValue()); + + + assertEquals("/a/", RadixTree.radixPathToString( + ROOT.getLongestPrefixPath("/a/g"))); + + } + + @Test + public void testGetLastNoeInPrefixPath() { + assertEquals(null, ROOT.getLastNodeInPrefixPath("/a/g")); + RadixNode ln = ROOT.getLastNodeInPrefixPath("/a/b/e/dir1"); + assertEquals("dir1", ln.getName()); + } + + @Test + public void testRemovePrefixPath() { + + // Remove, test and restore + // Remove partially overlapped path + ROOT.removePrefixPath("/a/b/c/d/g/h"); + assertEquals("/a/b/c", ROOT.getLongestPrefix("a/b/c/d")); + ROOT.insert("/a/b/c/d/g/h"); + + // Remove fully overlapped path + ROOT.removePrefixPath("/a/b/c/d"); + assertEquals("/a/b/c/d", ROOT.getLongestPrefix("a/b/c/d")); + ROOT.insert("/a/b/c/d"); + + // Remove non existing path + ROOT.removePrefixPath("/d/a"); + assertEquals("/a/b/c/d", ROOT.getLongestPrefix("a/b/c/d")); + } + + +} diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/package-info.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/package-info.java new file mode 100644 index 00000000000..a6acd30d77c --- /dev/null +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/util/package-info.java @@ -0,0 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.ozone.util; +/** + * Unit tests of generic ozone utils. + */