code: public ResourcePath join(String otherPath) {
      if (otherPath == null) {
        throw new IllegalArgumentException("The join(String) method cannot be called with a null argument");
      }
  
      DynamicArray<String> components = tokenizePathFragment(otherPath, 0);
      ResourcePath result = this;
      for (int i = 0; i < components.size; i++) {
        String s = (String) components.data[i];
        if (s.equals(".") || s.equals("/") || s.equals("\\") || s.equals("")) {
          // no-op
        } else if (s.equals("..")) {
          result = result.getParent();
          if (result == null) {
            throw new IllegalArgumentException("Joining the path [" + otherPath + "] to the base path [" + getPathString() + "] resulted in traversing backwards past the root path element");
          }
        } else {
          result = new ResourcePath(result, s);
        }
      }
  
      return result;
    }
docs: Takes the specified path fragment and joins it with this ResourcePath to create a new ResourcePath.
  The specified path can use either \ or / separator characters (or a mix of the two), and it can include
  . or .. elements in the path, which will be traversed appropriately.  The path can start and/or end with
  a separator character, but it should not start with a windows drive letter or network root.  The resulting
  path will have the same root as this path.  Some examples:
  <ul>
  <li>"/usr/local".join("lib/java") -> "/usr/local/lib/java"</li>
  <li>"/usr/local".join("/..") -> "/usr"</li>
  <li>"/usr/local".join("/.") -> "/usr/local</li>
  </ul>
  If .. path elements would lead to traversing backwards past the root element, an IllegalArgumentException will
  be thrown.  An IllegalArgumentException will also be thrown if otherPath is null.
  
  @param otherPath the path to join with this one
  @return a ResourcePath that results from appending otherPath to this path and then normalizing the result