diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/CategoryDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/CategoryDB.java index 255f2edda2..348b6be6c2 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/CategoryDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/CategoryDB.java @@ -57,13 +57,13 @@ class CategoryDB extends DatabaseObject implements Category { this.name = name; this.parent = parent; - subcategoryMap = new LazyLoadingCachingMap(mgr.lock, CategoryDB.class) { + subcategoryMap = new LazyLoadingCachingMap<>(mgr.lock, CategoryDB.class) { @Override public Map loadMap() { return buildSubcategoryMap(); } }; - dataTypeMap = new LazyLoadingCachingMap(mgr.lock, DataType.class) { + dataTypeMap = new LazyLoadingCachingMap<>(mgr.lock, DataType.class) { @Override public Map loadMap() { return createDataTypeMap(); @@ -273,9 +273,6 @@ class CategoryDB extends DatabaseObject implements Category { if (categoryName == null || categoryName.length() == 0) { throw new InvalidNameException("Name cannot be null or zero length"); } - if (categoryName.indexOf(DELIMITER_CHAR) >= 0) { - throw new InvalidNameException("Bad name: " + categoryName); - } } /** diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Category.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Category.java index da6806a0df..fd052237c8 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Category.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Category.java @@ -23,12 +23,6 @@ import ghidra.util.task.TaskMonitor; * Each data type resides in a given a category. */ public interface Category extends Comparable { - public static final char DELIMITER_CHAR = '/'; // delimeter between categories - - public static final String NAME_DELIMITER = "/"; // delimiter between names - - public static final String DELIMITER_STRING = "" + DELIMITER_CHAR; // delimeter between categories - /** * Get the name of this category. */ diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CategoryPath.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CategoryPath.java index a3070499e9..bad85eaff3 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CategoryPath.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CategoryPath.java @@ -15,7 +15,7 @@ */ package ghidra.program.model.data; -import java.util.StringTokenizer; +import java.util.*; /** * A category path is the full path to a particular data type @@ -24,97 +24,211 @@ public class CategoryPath implements Comparable { public static final char DELIMITER_CHAR = '/'; public static final String DELIMITER_STRING = "" + DELIMITER_CHAR; + public static final String ESCAPED_DELIMITER_STRING = "\\" + DELIMITER_STRING; - public static final CategoryPath ROOT = new CategoryPath(null); + public static final CategoryPath ROOT = new CategoryPath(); private static final String ILLEGAL_STRING = DELIMITER_STRING + DELIMITER_STRING; + private static final int DIFF = ESCAPED_DELIMITER_STRING.length() - DELIMITER_STRING.length(); - private final String parentPath; + // parentPath can only be null for ROOT + private final CategoryPath parentPath; private final String name; /** - * Create a category path given a string. + * Converts a non-escaped String into an escaped string suitable for being passed in as a + * component of a single category path string to the constructor that takes a single + * escaped category path string. The user is responsible for constructing the single + * category path string from the escaped components. + * @param nonEscapedString String that might need escaping for characters used for delimiting + * @return escaped String + * @see #unescapeString(String) + */ + public static String escapeString(String nonEscapedString) { + return nonEscapedString.replace(DELIMITER_STRING, ESCAPED_DELIMITER_STRING); + } + + /** + * Converts an escaped String suitable for being passed in as a component of a single category + * path string into an non-escaped string. + * @param escapedString String that might need unescaping for characters used for delimiting + * @return non-escaped String + * @see #escapeString(String) + */ + public static String unescapeString(String escapedString) { + return escapedString.replace(ESCAPED_DELIMITER_STRING, DELIMITER_STRING); + } + + /** + * Constructor for internal creation of ROOT. + */ + private CategoryPath() { + // parentPath can only be null for ROOT + parentPath = null; + name = ""; + } + + /** + * Construct a CategoryPath from a parent and a hierarchical array of strings where each + * string is the name of a category in the category path. * - * @param path category path string. + * @param parentPathIn the parent CategoryPath. Choose {@code ROOT} if needed. + * @param categoryPath the array of names of categories. + * @throws IllegalArgumentException if the given array is null or empty. + */ + public CategoryPath(CategoryPath parentPathIn, String... categoryPath) { + this(parentPathIn, Arrays.asList(categoryPath)); + } + + /** + * Construct a CategoryPath from a parent and a hierarchical list of strings where each + * string is the name of a category in the category path. + * + * @param parentPathIn the parent CategoryPath. Choose {@code ROOT} if needed. + * @param categoryList the hierarchical array of categories to place after parentPath. + * @throws IllegalArgumentException if the given list is null or empty. + */ + public CategoryPath(CategoryPath parentPathIn, List categoryList) { + Objects.requireNonNull(parentPathIn); + if (categoryList == null || categoryList.isEmpty()) { + throw new IllegalArgumentException( + "Category list must contain at least one string name!"); + } + name = categoryList.get(categoryList.size() - 1); + if (categoryList.size() == 1) { + parentPath = parentPathIn; + } + else { + parentPath = + new CategoryPath(parentPathIn, categoryList.subList(0, categoryList.size() - 1)); + } + } + + /** + * Creates a category path given a forward-slash-delimited string (e.g., {@code "/aa/bb"}). + * If an individual component has one or more '/' characters in it, then it can be + * escaped using the {@link #escapeString(String)} utility method. The + * {@link #unescapeString(String)} method can be used to unescape an individual component. + *

+ * Refrain from using this constructor in production code, and instead use one of the + * other constructors that does not require escaping. Situations where using this constructor + * is OK is in simple cases where a literal is passed in, such as in testing methods or in + * scripts. + * @param path category path string, delimited with '/' characters where individual components + * may have '/' characters escaped. */ public CategoryPath(String path) { if (path == null || path.length() == 0 || path.equals(DELIMITER_STRING)) { - this.parentPath = this.name = ""; + // parentPath can only be null for ROOT + parentPath = null; + name = ""; + return; } else if (path.charAt(0) != DELIMITER_CHAR) { throw new IllegalArgumentException("Paths must start with " + DELIMITER_STRING); } - else if (path.charAt(path.length() - 1) == DELIMITER_CHAR) { + else if (path.charAt(path.length() - 1) == DELIMITER_CHAR && + path.lastIndexOf(ESCAPED_DELIMITER_STRING) != path.length() - + ESCAPED_DELIMITER_STRING.length()) { throw new IllegalArgumentException("Paths must not end with " + DELIMITER_STRING); } else if (path.indexOf(ILLEGAL_STRING) >= 0) { throw new IllegalArgumentException("Paths must have non-empty elements"); } else { - int index = path.lastIndexOf(DELIMITER_CHAR); - this.parentPath = path.substring(0, index); - this.name = path.substring(index + 1); + int escapedIndex = path.length(); + int delimiterIndex = path.length(); + while (delimiterIndex > 0) { + escapedIndex = path.lastIndexOf(ESCAPED_DELIMITER_STRING, escapedIndex - 1); + delimiterIndex = path.lastIndexOf(DELIMITER_CHAR, delimiterIndex - 1); + if (delimiterIndex != escapedIndex + DIFF) { + break; + } + } + this.parentPath = new CategoryPath(path.substring(0, delimiterIndex)); + this.name = unescapeString(path.substring(delimiterIndex + 1)); } } - /** - * Create a category path given a parent category and name. - * - * @param parent parent category this path will reside in. - * @param name name of the category within the parent category. - */ - public CategoryPath(CategoryPath parent, String name) { - if (name == null || name.length() == 0 || name.indexOf(DELIMITER_CHAR) >= 0) { - throw new IllegalArgumentException("Bad name: " + name); - } - this.parentPath = parent.isRoot() ? "" : parent.getPath(); - this.name = name; - } - /** * Determine if this category path corresponds to the root category * @return true if this is a root category path */ public boolean isRoot() { - return parentPath.length() == 0 && name.length() == 0; + // parentPath can only be null for ROOT + return parentPath == null; } /** - * Return the name of this category path + * Return the parent category path. + * @return the parent + */ + public CategoryPath getParent() { + return parentPath; + } + + /** + * Return the terminating name of this category path. + * @return the name */ public String getName() { return name; } /** - * Return the full path to the category including the category name as a string. + * Return the {@link String} representation of this category path including the category name, + * where components are delimited with a forward slash. Any component that contains a forward + * slash will be have the forward slash characters escaped. + * @return the full category path */ public String getPath() { - return parentPath + DELIMITER_CHAR + name; - } - - /** - * Return the parent category path. - */ - public CategoryPath getParent() { - if (parentPath.length() == 0 && name.length() == 0) { - return null; + if (isRoot()) { + return DELIMITER_STRING; } - return new CategoryPath(parentPath); + if (parentPath.isRoot()) { + return DELIMITER_CHAR + escapeString(name); + } + return parentPath.getPath() + DELIMITER_CHAR + escapeString(name); } @Override public boolean equals(Object obj) { - if (obj instanceof CategoryPath) { - CategoryPath cp = (CategoryPath) obj; - return cp.parentPath.equals(parentPath) && cp.name.equals(name); + if (this == obj) { + return true; } - return false; + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CategoryPath other = (CategoryPath) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + if (parentPath == null) { + if (other.parentPath != null) { + return false; + } + } + else if (!parentPath.equals(other.parentPath)) { + return false; + } + return true; } @Override public int hashCode() { - return parentPath.hashCode() + name.hashCode(); + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((parentPath == null) ? 0 : parentPath.hashCode()); + return result; } /** @@ -124,8 +238,9 @@ public class CategoryPath implements Comparable { */ public boolean isAncestorOrSelf(CategoryPath categoryPath) { - // Result categoryPath This + // Result categoryPath This // ------ --------------------- ------------------------ + // True / / // True / /apple // False /apple / // True /apple /apple/sub @@ -137,39 +252,46 @@ public class CategoryPath implements Comparable { return true; } - if (isRoot()) { - return false; + CategoryPath path = this; + while (!path.isRoot()) { + if (categoryPath.equals(path)) { + return true; + } + path = path.getParent(); } - - String otherCategory = categoryPath.getPath(); - String myCategory = getPath(); - if (!myCategory.startsWith(otherCategory)) { - return false; - } - - if (myCategory.length() == otherCategory.length()) { - // categoryPath is the same as this - return true; - } - - return myCategory.charAt(otherCategory.length()) == DELIMITER_CHAR; + return false; } + /** + * Returns array of names in category path. + * @return array of names + */ public String[] getPathElements() { - StringTokenizer tokenizer = new StringTokenizer(getPath(), DELIMITER_STRING); - String[] tokens = new String[tokenizer.countTokens()]; - for (int i = 0; i < tokens.length; i++) { - tokens[i] = tokenizer.nextToken(); - } - return tokens; + return asArray(); } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override - public int compareTo(CategoryPath otherPath) { - return getPath().compareTo(otherPath.getPath()); + public int compareTo(CategoryPath other) { + CategoryPath otherParentPath = other.getParent(); + int result = 0; + if (parentPath == null) { + if (otherParentPath != null) { + return -1; + } + } + else if (otherParentPath == null) { + return 1; + } + else { + result = parentPath.compareTo(otherParentPath); + } + if (result == 0) { + result = name.compareTo(other.getName()); + } + return result; } /* (non-Javadoc) @@ -180,4 +302,51 @@ public class CategoryPath implements Comparable { return getPath(); } + /** + * Returns a hierarchical list of names of the categories in the category path, starting with + * the name just below the {@code ROOT} category. + * + * @return a hierarchical list of names of the category in the category path. + */ + public List asList() { + List list = new ArrayList<>(); + addToList(list); + return list; + } + + /** + * Returns a hierarchical array of names of the categories in the category path, starting with + * the name just below the {@code ROOT} category. + * + * @return a hierarchical array of names of the categories in the category path. + */ + public String[] asArray() { + List list = new ArrayList<>(); + addToList(list); + return list.toArray(new String[list.size()]); + } + + private void addToList(List list) { + if (!parentPath.isRoot()) { + parentPath.addToList(list); + } + list.add(name); + } + + /** + * This constructor is purposefully private and asserting. We do not want anyone to implement + * this constructor, as it would confuse the notion of the constructor that takes a single + * {@link String} that has escaped delimiters vs. what this constructor would have to require, + * which is non-escaped {@link String Strings}. There would be no way for the two Constructors + * to be distinguished from each other when passing a single argument. + * + * @param categoryPath varargs hierarchical list of names of categories. + */ + @SuppressWarnings("unused") // for categoryPath varargs + private CategoryPath(String... categoryPath) { + assert false; + parentPath = null; + name = ""; + } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypePath.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypePath.java index 221db0d521..796229c28b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypePath.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/DataTypePath.java @@ -39,19 +39,20 @@ public class DataTypePath { * @param dataTypeName the name of the datatype. * @throws IllegalArgumentException if a null category path or dataTypeName is given. */ - public DataTypePath(CategoryPath categoryPath, String datatypeName) { - if (datatypeName == null || categoryPath == null) { + public DataTypePath(CategoryPath categoryPath, String dataTypeName) { + if (dataTypeName == null || categoryPath == null) { throw new IllegalArgumentException("null not allowed for categoryPath or datatypeName"); } this.categoryPath = categoryPath; - this.dataTypeName = datatypeName; + this.dataTypeName = dataTypeName; } /** * Returns the categoryPath for the datatype represented by this datatype path. * (ie. the CategoryPath that contains the DataType that this DataTypePath points to). * - * @return the parent {@link CategoryPath} of the {@link DataType} that this DataTypePath points to. + * @return the parent {@link CategoryPath} of the {@link DataType} that this DataTypePath + * points to. */ public CategoryPath getCategoryPath() { return categoryPath; @@ -70,6 +71,7 @@ public class DataTypePath { /** * Returns the name of the datatype. + * @return the name */ public String getDataTypeName() { return dataTypeName; @@ -79,6 +81,7 @@ public class DataTypePath { * Returns the full path of this datatype. NOTE: if the datatype name contains any * "/" characters, then the resulting path string may be ambiguous as to where the * category path ends and the datatype name begins. + * @return the full path */ public String getPath() { StringBuffer buf = new StringBuffer(categoryPath.getPath()); diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/CategoryPathTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/CategoryPathTest.java index 0f4f607e14..c3ccaa8788 100644 --- a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/CategoryPathTest.java +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/CategoryPathTest.java @@ -21,18 +21,17 @@ */ package ghidra.program.model.data; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; -import org.junit.Assert; import org.junit.Test; import generic.test.AbstractGenericTest; /** - * - * - * To change the template for this generated type comment go to - * Window>Preferences>Java>Code Generation>Code and Comments + * {@link CategoryPath} tests. */ public class CategoryPathTest extends AbstractGenericTest { @@ -41,109 +40,285 @@ public class CategoryPathTest extends AbstractGenericTest { } @Test - public void testConstructor() { + public void testEscapeStringEmpty() { + String orig = ""; + String escaped = CategoryPath.escapeString(orig); + String unescaped = CategoryPath.unescapeString(escaped); + assertEquals(orig, unescaped); + assertEquals("", escaped); + } + + @Test + public void testEscapeString1() { + String orig = "/"; + String escaped = CategoryPath.escapeString(orig); + String unescaped = CategoryPath.unescapeString(escaped); + assertEquals(orig, unescaped); + assertEquals("\\/", escaped); + } + + @Test + public void testEscapeString2() { + String orig = "//"; + String escaped = CategoryPath.escapeString(orig); + String unescaped = CategoryPath.unescapeString(escaped); + assertEquals(orig, unescaped); + assertEquals("\\/\\/", escaped); + } + + @Test + public void testConstructorRoot1() { + CategoryPath c = CategoryPath.ROOT; + assertEquals("/", c.getPath()); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); + } + + @Test + public void testConstructorRoot2() { CategoryPath c = new CategoryPath(null); - Assert.assertEquals("/", c.getPath()); - Assert.assertEquals("", c.getName()); + assertEquals("/", c.getPath()); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); + } - c = new CategoryPath(""); - Assert.assertEquals("/", c.getPath()); - Assert.assertEquals("", c.getName()); + @Test + public void testConstructorRoot3() { + CategoryPath c = new CategoryPath(""); + assertEquals("/", c.getPath()); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); + } - c = new CategoryPath("/"); - Assert.assertEquals("/", c.getPath()); - Assert.assertEquals("", c.getName()); + @Test + public void testConstructorRoot4() { + CategoryPath c = new CategoryPath("/"); + assertEquals("/", c.getPath()); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); + } - c = new CategoryPath("/apple"); - Assert.assertEquals("/apple", c.getPath()); - Assert.assertEquals("apple", c.getName()); + @Test + public void testConstructorBasicString1() { + CategoryPath c = new CategoryPath("/apple"); + assertEquals("/apple", c.getPath()); + assertEquals("apple", c.getName()); + } - c = new CategoryPath("/apple/pear"); - Assert.assertEquals("/apple/pear", c.getPath()); - Assert.assertEquals("pear", c.getName()); + @Test + public void testConstructorBasicString2() { + CategoryPath c = new CategoryPath("/apple/pear"); + assertEquals("/apple/pear", c.getPath()); + assertEquals("pear", c.getName()); + } - try { - c = new CategoryPath("//"); - Assert.fail(); - } - catch (IllegalArgumentException e) { - } - try { - c = new CategoryPath("apple"); - Assert.fail(); - } - catch (IllegalArgumentException e) { - } - try { - c = new CategoryPath("/apple/"); - Assert.fail(); - } - catch (IllegalArgumentException e) { - } - try { - c = new CategoryPath("/apple//bob"); - Assert.fail(); - } - catch (IllegalArgumentException e) { - } + @Test + public void testConstructorParentVarargsSingle() { + CategoryPath c = new CategoryPath("/apple/pear"); + c = new CategoryPath(c, "mango"); + assertEquals("/apple/pear/mango", c.getPath()); + assertEquals("mango", c.getName()); + } + + @Test + public void testConstructorParentAndList() { + CategoryPath parent = new CategoryPath("/universe/earth"); + List list = new ArrayList<>(); + list.add("boy"); + list.add("bad"); + CategoryPath c = new CategoryPath(parent, list); + assertEquals("/universe/earth/boy/bad", c.getPath()); + assertEquals("bad", c.getName()); + } + + @Test + public void testConstructorParentAndVarargsArray() { + CategoryPath parent = new CategoryPath("/apple/peaches"); + CategoryPath c = new CategoryPath(parent, new String[] { "pumpkin", "pie" }); + assertEquals("pie", c.getName()); + c = c.getParent(); + assertEquals("pumpkin", c.getName()); + c = c.getParent(); + assertEquals("peaches", c.getName()); + c = c.getParent(); + assertEquals("apple", c.getName()); + c = c.getParent(); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); + } + + @Test + public void testConstructorParentAndVarargs() { + CategoryPath parent = new CategoryPath("/apple/peaches"); + CategoryPath c = new CategoryPath(parent, "pumpkin", "pie"); + assertEquals("pie", c.getName()); + c = c.getParent(); + assertEquals("pumpkin", c.getName()); + c = c.getParent(); + assertEquals("peaches", c.getName()); + c = c.getParent(); + assertEquals("apple", c.getName()); + c = c.getParent(); + assertEquals("", c.getName()); + assertTrue(c.isRoot()); } @Test(expected = IllegalArgumentException.class) - public void testBadCtorParam_empty_path_element() { + public void testConstructorBadCtorParam_empty_path_element() { new CategoryPath("//"); } @Test(expected = IllegalArgumentException.class) - public void testBadCtorParam_empty_path_element_2() { + public void testConstructorBadCtorParam_empty_path_element_2() { new CategoryPath("/apple//bob"); } @Test(expected = IllegalArgumentException.class) - public void testBadCtorParam_missing_leading_slash() { + public void testConstructorBadCtorParam_missing_leading_slash() { new CategoryPath("apple"); } @Test(expected = IllegalArgumentException.class) - public void testBadCtorParam_bad_trailing_slash() { + public void testConstructorBadCtorParam_bad_trailing_slash() { new CategoryPath("/apple/"); } - @Test - public void testOtherConstructor() { - CategoryPath a = new CategoryPath("/aaa"); - CategoryPath b = new CategoryPath(a, "bbb"); - Assert.assertEquals("/aaa/bbb", b.getPath()); - Assert.assertEquals("bbb", b.getName()); - } - @Test public void testGetParent() { - CategoryPath c = new CategoryPath(null); + CategoryPath c = CategoryPath.ROOT; assertNull(c.getParent()); - c = new CategoryPath("/aaa/bbb/ccc"); c = c.getParent(); - Assert.assertEquals("/aaa/bbb", c.getPath()); + assertEquals("/aaa/bbb", c.getPath()); } @Test - public void testIsAncestor() { - - Assert.assertTrue(CategoryPath.ROOT.isAncestorOrSelf(CategoryPath.ROOT)); + public void testIsAncestorRootRoot() { + assertTrue(CategoryPath.ROOT.isAncestorOrSelf(CategoryPath.ROOT)); + } + @Test + public void testIsAncestorRootApple() { CategoryPath apple = new CategoryPath("/apple"); - Assert.assertTrue(apple.isAncestorOrSelf(CategoryPath.ROOT)); - Assert.assertFalse(CategoryPath.ROOT.isAncestorOrSelf(apple)); + assertTrue(apple.isAncestorOrSelf(CategoryPath.ROOT)); + assertFalse(CategoryPath.ROOT.isAncestorOrSelf(apple)); + } + @Test + public void testIsAncestorAppleSubApple() { + CategoryPath apple = new CategoryPath("/apple"); CategoryPath applesub = new CategoryPath("/apple/sub"); - Assert.assertTrue(applesub.isAncestorOrSelf(apple)); - Assert.assertTrue(applesub.isAncestorOrSelf(applesub)); + assertTrue(applesub.isAncestorOrSelf(apple)); + assertTrue(applesub.isAncestorOrSelf(applesub)); + } + @Test + public void testIsAncestorAppleSubNotApple() { + CategoryPath applesub = new CategoryPath("/apple/sub"); CategoryPath notapple = new CategoryPath("/notapple"); - Assert.assertFalse(applesub.isAncestorOrSelf(notapple)); + assertFalse(applesub.isAncestorOrSelf(notapple)); + } + @Test + public void testIsAncestorAppleSubApp() { + CategoryPath applesub = new CategoryPath("/apple/sub"); CategoryPath app = new CategoryPath("/app"); - Assert.assertFalse(applesub.isAncestorOrSelf(app)); + assertFalse(applesub.isAncestorOrSelf(app)); + } + + @Test + public void testToArray() { + CategoryPath path = new CategoryPath("/aaa/bbb/bob"); + String[] names = path.asArray(); + assertEquals("aaa", names[0]); + assertEquals("bbb", names[1]); + assertEquals("bob", names[2]); + } + + @Test + public void testToList() { + CategoryPath path = new CategoryPath("/aaa/bbb/bob"); + List names = path.asList(); + assertEquals("aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("bob", names.get(2)); + } + + @Test + public void testConstructorDelimeterEscape1() { + CategoryPath path = new CategoryPath("/aaa/bbb/\\/bob"); + List names = path.asList(); + assertEquals("aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("/bob", names.get(2)); + assertEquals("/aaa/bbb/\\/bob", path.getPath()); + } + + @Test + public void testConstructorDelimeterEscape2() { + // Should not complain about terminating slash + CategoryPath path = new CategoryPath("/aaa/bbb/bob\\/"); + List names = path.asList(); + assertEquals("aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("bob/", names.get(2)); + assertEquals("/aaa/bbb/bob\\/", path.getPath()); + } + + @Test + public void testConstructorDelimeterEscape3() { + CategoryPath path = new CategoryPath("/\\/aaa/bbb/bob"); + List names = path.asList(); + assertEquals("/aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("bob", names.get(2)); + assertEquals("/\\/aaa/bbb/bob", path.getPath()); + } + + @Test + public void testConstructorDelimeterEscape4() { + CategoryPath path = new CategoryPath("/\\/aaa/bbb/bob"); + List names = path.asList(); + assertEquals("/aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("bob", names.get(2)); + assertEquals("/\\/aaa/bbb/bob", path.getPath()); + } + + @Test + public void testConstructorDelimeterEscape5() { + CategoryPath path = new CategoryPath("/\\/\\/aaa/bbb/bob"); + List names = path.asList(); + assertEquals("//aaa", names.get(0)); + assertEquals("bbb", names.get(1)); + assertEquals("bob", names.get(2)); + assertEquals("/\\/\\/aaa/bbb/bob", path.getPath()); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings("unused") + public void testDelimeterEscapeAtRoot() { + CategoryPath path = new CategoryPath("\\//aaa/bbb/bob"); + } + + @Test + public void testConstructorParentVarargsNestedDelimiter1() { + CategoryPath c = new CategoryPath("/apple/pear"); + // nested delimiter sequence should be ignored on constructor and getName(), but output on + // getPath(). + c = new CategoryPath(c, "man/go"); + assertEquals("/apple/pear/man\\/go", c.getPath()); + assertEquals("man/go", c.getName()); + } + + @Test + public void testConstructorParentVarargsNestedEscape1() { + CategoryPath c = new CategoryPath("/apple/pear"); + // nested escape sequence should be ignored on constructor and getName(), but output on + // getPath(). + c = new CategoryPath(c, "man\\/go"); + assertEquals("/apple/pear/man\\\\/go", c.getPath()); + assertEquals("man\\/go", c.getName()); } }