From c97e35377aea70cb293cabdd205bcc5da64b95c6 Mon Sep 17 00:00:00 2001
From: Sven Gothel <sgothel@jausoft.com>
Date: Wed, 10 Sep 2014 07:21:03 +0200
Subject: Bug 1063: Uri: Refine API doc; Add create(Encoded ..) ; Provide
 common impl. for getNormalized(), getDirectory(), getParent() and
 getRelativeOf()

- Refine API doc
  - Add notion of {@code host} and {@code port} validation

- Add create(Encoded ..), allowing creation of variants w/o re-encoding

- Provide common impl. for getNormalized(), getDirectory(), getParent() and getRelativeOf()
  Above feature methods share common goals, hence use same implementation:
  - If opaque, cut-off query and merge after operation
  - cleanup path, i.e. /dummy/../test/ -> /test/
  - cutoff file, dir - if requested
  - append optional appendix and cleanup again

  Return behavior various thought, i.e. null, this or allow exception.

Enhanced test of above features.
---
 src/java/com/jogamp/common/net/Uri.java           | 633 ++++++++++++++++------
 src/java/com/jogamp/common/net/UriQueryProps.java |   8 +-
 2 files changed, 482 insertions(+), 159 deletions(-)

(limited to 'src/java/com/jogamp/common')

diff --git a/src/java/com/jogamp/common/net/Uri.java b/src/java/com/jogamp/common/net/Uri.java
index a2c8833..6bafba2 100644
--- a/src/java/com/jogamp/common/net/Uri.java
+++ b/src/java/com/jogamp/common/net/Uri.java
@@ -435,6 +435,8 @@ public class Uri {
         /** See {@link String#lastIndexOf(String, int)}. */
         public int lastIndexOf(final String str, final int fromIndex) { return s.lastIndexOf(str, fromIndex); }
 
+        /** See {@link String#startsWith(String)} */
+        public boolean startsWith(final String prefix) { return s.startsWith(prefix); }
         /** See {@link String#startsWith(String, int)} */
         public boolean startsWith(final String prefix, final int toffset) { return s.startsWith(prefix, toffset); }
         /** See {@link String#endsWith(String)} */
@@ -631,14 +633,17 @@ public class Uri {
     }
 
     /**
-     * Creates a new Uri instance using the given arguments.
+     * Creates a new Uri instance using the given unencoded arguments.
      * <p>
-     * This constructor first creates a temporary Uri string from the given components. This
+     * This constructor first creates a temporary Uri string from the given unencoded components. This
      * string will be parsed later on to create the Uri instance.
      * </p>
      * <p>
      * {@code [scheme:]scheme-specific-part[#fragment]}
      * </p>
+     * <p>
+     * {@code host} and {@code port} <i>may</i> be undefined or invalid within {@code scheme-specific-part}.
+     * </p>
      *
      * @param scheme the unencoded scheme part of the Uri.
      * @param ssp the unencoded scheme-specific-part of the Uri.
@@ -669,19 +674,66 @@ public class Uri {
     }
 
     /**
-     * Creates a new Uri instance using the given arguments.
+     * Creates a new Uri instance using the given encoded arguments.
      * <p>
-     * This constructor first creates a temporary Uri string from the given components. This
+     * This constructor first creates a temporary Uri string from the given encoded components. This
+     * string will be parsed later on to create the Uri instance.
+     * </p>
+     * <p>
+     * The given encoded components are taken as-is, i.e. no re-encoding will be performed!
+     * However, Uri parsing will re-evaluate encoding of the resulting components.
+     * </p>
+     * <p>
+     * {@code [scheme:]scheme-specific-part[#fragment]}
+     * </p>
+     * <p>
+     * {@code host} and {@code port} <i>may</i> be undefined or invalid within {@code scheme-specific-part}.
+     * </p>
+     *
+     * @param scheme the encoded scheme part of the Uri.
+     * @param ssp the encoded scheme-specific-part of the Uri.
+     * @param fragment the encoded fragment part of the Uri.
+     * @throws URISyntaxException
+     *             if the temporary created string doesn't fit to the
+     *             specification RFC2396 or could not be parsed correctly.
+     */
+    public static Uri create(final Encoded scheme, final Encoded ssp, final Encoded fragment) throws URISyntaxException {
+        if ( emptyString(scheme) && emptyString(ssp) && emptyString(fragment) ) {
+            throw new URISyntaxException("", "all empty parts");
+        }
+        final StringBuilder uri = new StringBuilder();
+        if ( !emptyString(scheme) ) {
+            uri.append(scheme);
+            uri.append(SCHEME_SEPARATOR);
+        }
+        if ( !emptyString(ssp) ) {
+            uri.append(ssp.get());
+        }
+        if ( !emptyString(fragment) ) {
+            uri.append(FRAGMENT_SEPARATOR);
+            uri.append(fragment.get());
+        }
+        return new Uri(new Encoded(uri.toString()), false, 0);
+    }
+
+    /**
+     * Creates a new Uri instance using the given unencoded arguments.
+     * <p>
+     * This constructor first creates a temporary Uri string from the given unencoded components. This
      * string will be parsed later on to create the Uri instance.
      * </p>
      * <p>
      * {@code [scheme:][user-info@]host[:port][path][?query][#fragment]}
      * </p>
+     * <p>
+     * {@code host} and {@code port} <i>must</i> be defined and valid, if any {@code authority} components are defined,
+     * i.e. {@code user-info}, {@code host} or {@code port}.
+     * </p>
      *
      * @param scheme the unencoded scheme part of the Uri.
-     * @param userinfo the unencoded user information of the Uri for authentication and authorization.
-     * @param host the unencoded host name of the Uri.
-     * @param port the port number of the Uri.
+     * @param userinfo the unencoded user information of the Uri for authentication and authorization, {@code null} for undefined.
+     * @param host the unencoded host name of the Uri, {@code null} for undefined.
+     * @param port the port number of the Uri, -1 for undefined.
      * @param path the unencoded path to the resource on the host.
      * @param query the unencoded query part of the Uri to specify parameters for the resource.
      * @param fragment the unencoded fragment part of the Uri.
@@ -751,14 +803,97 @@ public class Uri {
     }
 
     /**
-     * Creates a new Uri instance using the given arguments.
+     * Creates a new Uri instance using the given encoded arguments.
      * <p>
-     * This constructor first creates a temporary Uri string from the given components. This
+     * This constructor first creates a temporary Uri string from the given encoded components. This
+     * string will be parsed later on to create the Uri instance.
+     * </p>
+     * <p>
+     * The given encoded components are taken as-is, i.e. no re-encoding will be performed!
+     * However, Uri parsing will re-evaluate encoding of the resulting components.
+     * </p>
+     * <p>
+     * {@code [scheme:][user-info@]host[:port][path][?query][#fragment]}
+     * </p>
+     * <p>
+     * {@code host} and {@code port} <i>must</i> be defined and valid, if any {@code authority} components are defined,
+     * i.e. {@code user-info}, {@code host} or {@code port}.
+     * </p>
+     *
+     * @param scheme the encoded scheme part of the Uri.
+     * @param userinfo the encoded user information of the Uri for authentication and authorization, {@code null} for undefined.
+     * @param host the encoded host name of the Uri, {@code null} for undefined.
+     * @param port the port number of the Uri, -1 for undefined.
+     * @param path the encoded path to the resource on the host.
+     * @param query the encoded query part of the Uri to specify parameters for the resource.
+     * @param fragment the encoded fragment part of the Uri.
+     * @throws URISyntaxException
+     *             if the temporary created string doesn't fit to the
+     *             specification RFC2396 or could not be parsed correctly.
+     */
+    public static Uri create (final Encoded scheme, final Encoded userinfo, final Encoded host, final int port,
+                              final Encoded path, final Encoded query, final Encoded fragment) throws URISyntaxException {
+        if ( emptyString(scheme) && emptyString(userinfo) && emptyString(host) && emptyString(path) &&
+             emptyString(query)  && emptyString(fragment) ) {
+            throw new URISyntaxException("", "all empty parts");
+        }
+
+        if ( !emptyString(scheme) && !emptyString(path) && path.length() > 0 && path.charAt(0) != '/') {
+            throw new URISyntaxException(path.get(), "path doesn't start with '/'");
+        }
+
+        final StringBuilder uri = new StringBuilder();
+        if ( !emptyString(scheme) ) {
+            uri.append(scheme);
+            uri.append(SCHEME_SEPARATOR);
+        }
+
+        if ( !emptyString(userinfo) || !emptyString(host) || port != -1) {
+            uri.append("//");
+        }
+
+        if ( !emptyString(userinfo) ) {
+            uri.append(userinfo.get());
+            uri.append('@');
+        }
+
+        if ( !emptyString(host) ) {
+            uri.append(host.get());
+        }
+
+        if ( port != -1 ) {
+            uri.append(SCHEME_SEPARATOR);
+            uri.append(port);
+        }
+
+        if ( !emptyString(path) ) {
+            uri.append(path.get());
+        }
+
+        if ( !emptyString(query) ) {
+            uri.append(QUERY_SEPARATOR);
+            uri.append(query.get());
+        }
+
+        if ( !emptyString(fragment) ) {
+            uri.append(FRAGMENT_SEPARATOR);
+            uri.append(fragment.get());
+        }
+        return new Uri(new Encoded(uri.toString()), true, 0);
+    }
+
+    /**
+     * Creates a new Uri instance using the given unencoded arguments.
+     * <p>
+     * This constructor first creates a temporary Uri string from the given unencoded components. This
      * string will be parsed later on to create the Uri instance.
      * </p>
      * <p>
      * {@code [scheme:]host[path][#fragment]}
      * </p>
+     * <p>
+     * {@code host} <i>must</i> be valid, if defined.
+     * </p>
      *
      * @param scheme the unencoded scheme part of the Uri.
      * @param host the unencoded host name of the Uri.
@@ -773,14 +908,46 @@ public class Uri {
     }
 
     /**
-     * Creates a new Uri instance using the given arguments.
+     * Creates a new Uri instance using the given encoded arguments.
      * <p>
-     * This constructor first creates a temporary Uri string from the given components. This
+     * This constructor first creates a temporary Uri string from the given encoded components. This
+     * string will be parsed later on to create the Uri instance.
+     * </p>
+     * <p>
+     * The given encoded components are taken as-is, i.e. no re-encoding will be performed!
+     * However, Uri parsing will re-evaluate encoding of the resulting components.
+     * </p>
+     * <p>
+     * {@code [scheme:]host[path][#fragment]}
+     * </p>
+     * <p>
+     * {@code host} <i>must</i> be valid, if defined.
+     * </p>
+     *
+     * @param scheme the encoded scheme part of the Uri.
+     * @param host the encoded host name of the Uri.
+     * @param path the encoded path to the resource on the host.
+     * @param fragment the encoded fragment part of the Uri.
+     * @throws URISyntaxException
+     *             if the temporary created string doesn't fit to the
+     *             specification RFC2396 or could not be parsed correctly.
+     */
+    public static Uri create(final Encoded scheme, final Encoded host, final Encoded path, final Encoded fragment) throws URISyntaxException {
+        return create(scheme, null, host, -1, path, null, fragment);
+    }
+
+    /**
+     * Creates a new Uri instance using the given unencoded arguments.
+     * <p>
+     * This constructor first creates a temporary Uri string from the given unencoded components. This
      * string will be parsed later on to create the Uri instance.
      * </p>
      * <p>
      * {@code [scheme:][//authority][path][?query][#fragment]}
      * </p>
+     * <p>
+     * {@code host} and {@code port} <i>may</i> be undefined or invalid, in the optional {@code authority}.
+     * </p>
      *
      * @param scheme the unencoded scheme part of the Uri.
      * @param authority the unencoded authority part of the Uri.
@@ -829,6 +996,66 @@ public class Uri {
         return new Uri(new Encoded(uri.toString()), false, 0);
     }
 
+    /**
+     * Creates a new Uri instance using the given encoded arguments.
+     * <p>
+     * This constructor first creates a temporary Uri string from the given encoded encoded components. This
+     * string will be parsed later on to create the Uri instance.
+     * </p>
+     * <p>
+     * The given encoded components are taken as-is, i.e. no re-encoding will be performed!
+     * However, Uri parsing will re-evaluate encoding of the resulting components.
+     * </p>
+     * <p>
+     * {@code [scheme:][//authority][path][?query][#fragment]}
+     * </p>
+     * <p>
+     * {@code host} and {@code port} <i>may</i> be undefined or invalid, in the optional {@code authority}.
+     * </p>
+     *
+     * @param scheme the encoded scheme part of the Uri.
+     * @param authority the encoded authority part of the Uri.
+     * @param path the encoded path to the resource on the host.
+     * @param query the encoded query part of the Uri to specify parameters for the resource.
+     * @param fragment the encoded fragment part of the Uri.
+     *
+     * @throws URISyntaxException
+     *             if the temporary created string doesn't fit to the
+     *             specification RFC2396 or could not be parsed correctly.
+     */
+    public static Uri create(final Encoded scheme, final Encoded authority, final Encoded path, final Encoded query, final Encoded fragment) throws URISyntaxException {
+        if ( emptyString(scheme) && emptyString(authority) && emptyString(path) &&
+             emptyString(query)  && emptyString(fragment) ) {
+            throw new URISyntaxException("", "all empty parts");
+        }
+        if ( !emptyString(scheme) && !emptyString(path) && path.length() > 0 && path.charAt(0) != '/') {
+            throw new URISyntaxException(path.get(), "path doesn't start with '/'");
+        }
+
+        final StringBuilder uri = new StringBuilder();
+        if ( !emptyString(scheme) ) {
+            uri.append(scheme);
+            uri.append(SCHEME_SEPARATOR);
+        }
+        if ( !emptyString(authority) ) {
+            uri.append("//");
+            uri.append(authority.get());
+        }
+
+        if ( !emptyString(path) ) {
+            uri.append(path.get());
+        }
+        if ( !emptyString(query) ) {
+            uri.append(QUERY_SEPARATOR);
+            uri.append(query.get());
+        }
+        if ( !emptyString(fragment) ) {
+            uri.append(FRAGMENT_SEPARATOR);
+            uri.append(fragment.get());
+        }
+        return new Uri(new Encoded(uri.toString()), false, 0);
+    }
+
     /**
      * Casts the given encoded String to a {@link Encoded#cast(String) new Encoded instance}
      * used to create the resulting Uri instance via {@link #Uri(Encoded)}.
@@ -851,7 +1078,7 @@ public class Uri {
      * {@code file:path}
      * </p>
      *
-     * @param path the path of the {@code file} {@code schema}.
+     * @param path the unencoded path of the {@code file} {@code schema}.
      * @throws URISyntaxException
      *             if the temporary created string doesn't fit to the
      *             specification RFC2396 or could not be parsed correctly.
@@ -913,13 +1140,9 @@ public class Uri {
             // opaque, without host validation.
             // Note: This may induce encoding errors of authority and path, see {@link #PARSE_HINT_FIX_PATH}
             return new Uri(new Encoded( uri.toString() ), false, 0);
-        } else if( null != uri.getHost() ) {
-            // with host validation
-            return Uri.create(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
-                              uri.getPath(), uri.getQuery(), uri.getFragment());
         } else {
-            // without host validation
-            return Uri.create(uri.getScheme(), uri.getAuthority(),
+            // with host validation if authority is defined
+            return Uri.create(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
                               uri.getPath(), uri.getQuery(), uri.getFragment());
         }
     }
@@ -1113,7 +1336,7 @@ public class Uri {
      * </p>
      */
     public final File toFile() {
-        if( isFileScheme() ) {
+        if( isFileScheme() && !emptyString(path) ) {
             final String authorityS;
             if( null == authority ) {
                 authorityS = "";
@@ -1154,10 +1377,10 @@ public class Uri {
      *     Returned Uri:  <code><i>scheme2</i>:/some/path/gluegen-rt.jar#fragment</code>
      *
      * Example 3:
-     *     This instance: <code>scheme1:<i>scheme2</i>:/some/path/gluegen-rt.jar?lala=01#fragment</code>
+     *     This instance: <code>scheme1:<i>scheme2</i>:/some/path/gluegen-rt.jar!/?lala=01#fragment</code>
      *     Returned Uri:  <code><i>scheme2</i>:/some/path/gluegen-rt.jar?lala=01#fragment</code>
      * </pre>
-     * @throws URISyntaxException
+     * @throws URISyntaxException if this Uri is a container Uri and does not comply with the container spec, i.e. a JAR Uri
      */
     public final Uri getContainedUri() throws URISyntaxException {
         if( !emptyString(schemeSpecificPart) ) {
@@ -1185,6 +1408,7 @@ public class Uri {
             } catch(final URISyntaxException e) {
                 // OK, does not contain uri
                 if( DEBUG ) {
+                    System.err.println("Caught "+e.getClass().getSimpleName()+": "+e.getMessage());
                     e.printStackTrace();
                 }
             }
@@ -1192,184 +1416,283 @@ public class Uri {
         return null;
     }
 
+    private static final boolean cutoffLastPathSegementImpl(final StringBuilder pathBuf,
+                                                            final boolean cutoffFile,
+                                                            final boolean cutoffDir,
+                                                            final Encoded appendPath) throws URISyntaxException {
+        final boolean cleaned;
+        {// clean-up existing path
+            final String pathS = pathBuf.toString();
+            if( 0 > pathS.indexOf("/") && emptyString(appendPath) ) {
+                return false; // nothing to cut-off
+            }
+            pathBuf.setLength(0);
+            pathBuf.append( IOUtil.cleanPathString( pathS ) );
+            cleaned = pathBuf.length() != pathS.length();
+        }
+
+        {// cut-off file or last dir-segment
+            final String pathS = pathBuf.toString();
+            final int jarSepIdx = pathS.lastIndexOf(JAR_SCHEME_SEPARATOR);
+            final int e = pathS.lastIndexOf("/");
+            if( 0 > jarSepIdx || e - 1 > jarSepIdx ) { // stop at jar-separator '!/', if exist
+                if( cutoffFile && e < pathS.length() - 1 ) {
+                    // cut-off file
+                    pathBuf.setLength(0);
+                    pathBuf.append( pathS.substring(0, e+1) );
+                } else if( cutoffDir ) {
+                    // cut-off dir-segment
+                    final int p = pathS.lastIndexOf("/", e-1);
+                    if( p >= 0 ) {
+                        pathBuf.setLength(0);
+                        pathBuf.append( pathS.substring(0, p+1) );
+                    } // else keep
+                } // else keep
+            }
+            final boolean cutoff = pathBuf.length() != pathS.length();
+            if( !cutoff && ( cutoffDir || !cleaned ) && emptyString(appendPath) ) {
+                return false; // no modifications!
+            }
+        }
+        if( !emptyString(appendPath) ) {
+            pathBuf.append(appendPath.get());
+            // 2nd round of cleaning!
+            final String pathS = pathBuf.toString();
+            pathBuf.setLength(0);
+            pathBuf.append( IOUtil.cleanPathString( pathS ) );
+        }
+        return true; // continue processing w/ buffer
+    }
+    private final Uri cutoffLastPathSegementImpl(final boolean cutoffFile, final boolean cutoffDir, final Encoded appendPath) throws URISyntaxException {
+        if( opaque ) {
+            if( emptyString(schemeSpecificPart) ) {
+                 // nothing to cut-off
+                if( !emptyString(appendPath) )  {
+                    return Uri.create(scheme, appendPath, fragment);
+                } else {
+                    return null;
+                }
+            }
+            final StringBuilder sspBuf = new StringBuilder(); // without path!
+
+            // save optional query in scheme-specific-part
+            final Encoded queryTemp;
+            final int queryI = schemeSpecificPart.lastIndexOf(QUERY_SEPARATOR);
+            if( queryI >= 0 ) {
+                queryTemp = schemeSpecificPart.substring(queryI+1);
+                sspBuf.append( schemeSpecificPart.substring(0, queryI).get() );
+            } else {
+                queryTemp = null;
+                sspBuf.append( schemeSpecificPart.get() );
+            }
+
+            if( !cutoffLastPathSegementImpl(sspBuf, cutoffFile, cutoffDir, appendPath) ) {
+                return null; // no modifications
+            }
+
+            if ( !emptyString(queryTemp)  ) {
+                sspBuf.append(QUERY_SEPARATOR);
+                sspBuf.append( queryTemp.get() );
+            }
+
+            // without host validation if authority is defined
+            return Uri.create(scheme, new Encoded(sspBuf.toString()), fragment);
+        } else {
+            if( emptyString(path) ) {
+                return null; // nothing to cut-off
+            }
+            final StringBuilder pathBuf = new StringBuilder();
+            pathBuf.append( path.get() );
+
+            if( !cutoffLastPathSegementImpl(pathBuf, cutoffFile, cutoffDir, appendPath) ) {
+                return null; // no modifications
+            }
+
+            // with host validation if authority is defined
+            return Uri.create(scheme, userInfo, host, port, new Encoded(pathBuf.toString()), query, fragment);
+        }
+    }
 
     /**
-     * Return a new Uri instance representing the parent path of this Uri,
-     * while cutting of optional {@code query} and {@code fragment} parts.
-     * <p>
-     * Method is {@code jar-file-entry} aware, i.e. will return the parent entry if exists.
-     * </p>
+     * {@link IOUtil#cleanPathString(String) Normalizes} this Uri's path and return the
+     * {@link IOUtil#cleanPathString(String) normalized} form if it differs, otherwise {@code this} instance.
      * <p>
-     * If this Uri does not contain any path separator, or a parent folder Uri cannot be found, method returns {@code null}.
-     * </p>
      * <pre>
      * Example-1:
-     *     This instance  : <code>jar:http://some/path/gluegen-rt.jar!/com/jogamp/common/GlueGenVersion.class</code>
-     *     Returned Uri #1: <code>jar:http://some/path/gluegen-rt.jar!/com/jogamp/common/</code>
-     *     Returned Uri #2: <code>jar:http://some/path/gluegen-rt.jar!/com/jogamp/</code>
+     *     This instance  : <code>jar:http://some/path/../gluegen-rt.jar!/com/Test.class?arg=1#frag</code>
+     *     Normalized     : <code>jar:http://some/gluegen-rt.jar!/com/Test.class?arg=1#frag</code>
      *
      * Example-2:
-     *     This instance  : <code>http://some/path/gluegen-rt.jar</code>
-     *     Returned Uri #1: <code>http://some/path/</code>
-     *     Returned Uri #2: <code>http://some/</code>
+     *     This instance  : <code>http://some/path/../gluegen-rt.jar?arg=1#frag</code>
+     *     Normalized     : <code>http://some/gluegen-rt.jar?arg=1#frag</code>
      * </pre>
+     * </p>
      */
-    public final Uri getParent() {
-        final int pl = null!=schemeSpecificPart? schemeSpecificPart.length() : 0;
-        if(pl != 0) {
-            final int e = schemeSpecificPart.lastIndexOf("/");
-            if( e > 0 ) { // 0 == e: no path
-                if( e <  pl - 1 ) {
-                    // path is file or has a query
-                    try {
-                        return new Uri( new Encoded( scheme.get()+SCHEME_SEPARATOR+schemeSpecificPart.get().substring(0, e+1) ) );
-                    } catch (final URISyntaxException ue) {
-                        // not complete, hence removed authority, or even root folder -> return null
-                    }
-                }
-                // path is a directory ..
-                final int p = schemeSpecificPart.lastIndexOf("/", e-1);
-                if( p > 0 ) {
-                    try {
-                        return new Uri( new Encoded( scheme.get()+SCHEME_SEPARATOR+schemeSpecificPart.get().substring(0, p+1) ) );
-                    } catch (final URISyntaxException ue) {
-                        // not complete, hence removed authority, or even root folder -> return null
-                    }
-                }
+    public final Uri getNormalized() {
+        try {
+            final Uri res = cutoffLastPathSegementImpl(false, false, null);
+            return null != res ? res : this;
+        } catch (final URISyntaxException e) {
+            if( DEBUG ) {
+                System.err.println("Caught "+e.getClass().getSimpleName()+": "+e.getMessage());
+                e.printStackTrace();
             }
-        }
-        return null;
-    }
-
-    /**
-     * Concatenates the given encoded string to the {@link #getEncoded() encoded uri}
-     * of this instance and returns {@link #Uri(Encoded) a new Uri instance} with the result.
-     *
-     * @throws URISyntaxException
-     *             if the concatenated string {@code uri} doesn't fit to the
-     *             specification RFC2396 and RFC3986 or could not be parsed correctly.
-     */
-    public final Uri concat(final Uri.Encoded suffix) throws URISyntaxException {
-        if( null == suffix ) {
             return this;
-        } else {
-            return new Uri( input.concat(suffix) );
         }
     }
 
     /**
-     * Returns a new Uri instance w/ the given new query {@code newQuery}.
+     * Returns this Uri's directory Uri.
+     * <p>
+     * This Uri path will be {@link IOUtil#cleanPathString(String) normalized} before returning the directory.
+     * </p>
+     * <p>
+     * If this Uri's directory cannot be found, or already denotes a directory, method returns {@code this} instance.
+     * </p>
+     * <p>
+     * <pre>
+     * Example-1:
+     *     this-uri: http:/some/path/gluegen-rt.jar?arg=1#frag
+     *     result:   http:/some/path/?arg=1#frag
      *
-     * @throws URISyntaxException if this Uri is {@link #opaque}
-     *             or if the new string {@code uri} doesn't fit to the
-     *             specification RFC2396 and RFC3986 or could not be parsed correctly.
+     * Example-2:
+     *     this-uri: file:/some/path/
+     *     result:   file:/some/path/
+     *
+     * Example-3:
+     *     this-uri: file:/some/path/lala/lili/../../hello.txt
+     *     result:   file:/some/path/
+     * </pre>
+     * </p>
+     * @throws URISyntaxException if the new string {@code uri} doesn't fit to the
+     *                            specification RFC2396 and RFC3986 or could not be parsed correctly.
      */
-    public final Uri getNewQuery(final String newQuery) throws URISyntaxException {
-        if( opaque ) {
-            throw new URISyntaxException(input.decode(), "Opaque Uri cannot permute by query");
-        } else if( null != host ) {
-            // with host validation
-            return Uri.create(decode(scheme), decode(userInfo), decode(host), port,
-                              decode(path), newQuery, decode(fragment));
-        } else {
-            // without host validation
-            return Uri.create(decode(scheme), decode(authority),
-                              decode(path), newQuery, decode(fragment));
+    public Uri getDirectory() {
+        try {
+            final Uri res = cutoffLastPathSegementImpl(true, false, null);
+            return null != res ? res : this;
+        } catch (final URISyntaxException e) {
+            if( DEBUG ) {
+                System.err.println("Caught "+e.getClass().getSimpleName()+": "+e.getMessage());
+                e.printStackTrace();
+            }
+            return this;
         }
     }
 
-    /// NEW START
-
     /**
-     * The URI's <code><i>protocol</i>:/some/path/gluegen-rt.jar</code>
-     * parent dirname URI <code><i>protocol</i>:/some/path/</code> will be returned,
-     * or {@code null} if not applicable.
+     * Returns this Uri's parent directory Uri..
      * <p>
-     * <i>protocol</i> may be "file", "http", etc..
+     * This Uri path will be {@link IOUtil#cleanPathString(String) normalized} before traversing up one directory.
      * </p>
+     * <p>
+     * If a parent folder cannot be found, method returns {@code null}.
+     * </p>
+     * <p>
+     * <pre>
+     * Example-1:
+     *     This instance  : <code>jar:http://some/path/gluegen-rt.jar!/com/Test.class?arg=1#frag</code>
+     *     Returned Uri #1: <code>jar:http://some/path/gluegen-rt.jar!/com/?arg=1#frag</code>
+     *     Returned Uri #2: <code>jar:http://some/path/gluegen-rt.jar!/?arg=1#frag</code>
+     *     Returned Uri #3: <code>null</code>
      *
-     * @return "<i>protocol</i>:/some/path/"
-     * @throws IllegalArgumentException if the URI doesn't match the expected formatting, or is null
-     * @throws URISyntaxException
+     * Example-2:
+     *     This instance  : <code>http://some/path/gluegen-rt.jar?arg=1#frag</code>
+     *     Returned Uri #1: <code>http://some/path/?arg=1#frag</code>
+     *     Returned Uri #2: <code>http://some/?arg=1#frag</code>
+     *     Returned Uri #2: <code>null</code>
+     *
+     * Example-3:
+     *     This instance  : <code>http://some/path/../gluegen-rt.jar?arg=1#frag</code>
+     *     Returned Uri #1: <code>http://some/?arg=1#frag</code>
+     *     Returned Uri #2: <code>null</code>
+     * </pre>
+     * </p>
      */
-    public Uri getDirectory() throws URISyntaxException {
-        final String uriS = input.get();
-
-        // from
-        //   file:/some/path/gluegen-rt.jar  _or_ rsrc:gluegen-rt.jar
-        // to
-        //   file:/some/path/                _or_ rsrc:
-        int idx = uriS.lastIndexOf('/');
-        if(0 > idx) {
-            // no abs-path, check for protocol terminator ':'
-            idx = uriS.lastIndexOf(':');
-            if(0 > idx) {
-                throw new URISyntaxException(input.get(), "no scheme terminator ':'");
-            }
-        }
+    public final Uri getParent() {
         try {
-            return Uri.cast(uriS.substring(0, idx+1)); // exclude jar name, include terminal '/' or ':'
-        } catch (final URISyntaxException ue) {
+            return cutoffLastPathSegementImpl(true, true, null);
+        } catch (final URISyntaxException e) {
             if( DEBUG ) {
-                ue.printStackTrace();
+                System.err.println("Caught "+e.getClass().getSimpleName()+": "+e.getMessage());
+                e.printStackTrace();
             }
+            return null;
         }
-        return null;
     }
 
     /**
-     * Generates a URI for the <i>relativePath</i> relative to the <i>baseURI</i>,
-     * hence the result is a absolute location.
+     * Returns a new Uri appending the given {@code appendPath}
+     * to this instance's {@link #getDirectory() directory}.
      * <p>
-     * Impl. operates on the <i>scheme-specific-part</i>, and hence is sub-protocol savvy.
+     * If {@code appendPath} is empty, method behaves like {@link #getNormalized()}.
      * </p>
      * <p>
-     * In case <i>baseURI</i> is not a path ending w/ '/', it's a assumed to be a file and it's parent is being used.
+     * This resulting path will be {@link IOUtil#cleanPathString(String) normalized}.
      * </p>
+     * <p>
+     * <pre>
+     * Example-1:
+     *     append: null
+     *     this-uri: http:/some/path/gluegen-rt.jar
+     *     result:   http:/some/path/gluegen-rt.jar
      *
-     * @param baseURI denotes a URI to a directory ending w/ '/', or a file. In the latter case the file's directory is being used.
-     * @param relativePath denotes a relative file to the baseLocation's parent directory (URI encoded)
-     * @throws URISyntaxException if path is empty or has no parent directory available while resolving <code>../</code>
+     * Example-2:
+     *     append: test.txt
+     *     this-uri: file:/some/path/gluegen-rt.jar
+     *     result:   file:/some/path/test.txt
+     *
+     * Example-3:
+     *     append: test.txt
+     *     this-uri: file:/some/path/lala/lili/../../hello.txt
+     *     result:   file:/some/path/test.txt
+     * </pre>
+     * </p>
+     *
+     * @param appendPath denotes a relative path to be appended to this Uri's directory
+     * @throws URISyntaxException
+     *             if the resulting {@code uri} doesn't fit to the
+     *             specification RFC2396 and RFC3986 or could not be parsed correctly.
      */
-    public Uri getRelativeOf(final Encoded relativePath) throws URISyntaxException {
-        return compose(scheme, schemeSpecificPart, relativePath, fragment);
+    public Uri getRelativeOf(final Encoded appendPath) throws URISyntaxException {
+        if( emptyString(appendPath) ) {
+            return getNormalized();
+        } else {
+            return cutoffLastPathSegementImpl(true, false, appendPath);
+        }
     }
 
-    static Uri compose(final Encoded scheme, final Encoded schemeSpecificPart, final Encoded relativePath, final Encoded fragment) throws URISyntaxException {
-        String schemeSpecificPartS = schemeSpecificPart.get();
-
-        // cut off optional query in scheme-specific-part
-        final String query;
-        final int queryI = schemeSpecificPartS.lastIndexOf(QUERY_SEPARATOR);
-        if( queryI >= 0 ) {
-            query = schemeSpecificPartS.substring(queryI+1);
-            schemeSpecificPartS = schemeSpecificPartS.substring(0, queryI);
+    /**
+     * Concatenates the given encoded string to the {@link #getEncoded() encoded uri}
+     * of this instance and returns {@link #Uri(Encoded) a new Uri instance} with the result.
+     *
+     * @throws URISyntaxException
+     *             if the concatenated string {@code uri} doesn't fit to the
+     *             specification RFC2396 and RFC3986 or could not be parsed correctly.
+     */
+    public final Uri concat(final Encoded suffix) throws URISyntaxException {
+        if( null == suffix ) {
+            return this;
         } else {
-            query = null;
-        }
-        if( null != relativePath ) {
-            if( !schemeSpecificPartS.endsWith("/") ) {
-                schemeSpecificPartS = IOUtil.getParentOf(schemeSpecificPartS);
-            }
-            schemeSpecificPartS = schemeSpecificPartS + relativePath.get();
-        }
-        schemeSpecificPartS = IOUtil.cleanPathString( schemeSpecificPartS );
-        final StringBuilder uri = new StringBuilder();
-        uri.append(scheme.get());
-        uri.append(':');
-        uri.append(schemeSpecificPartS);
-        if ( null != query ) {
-            uri.append(QUERY_SEPARATOR);
-            uri.append(query);
-        }
-        if ( null != fragment ) {
-            uri.append(FRAGMENT_SEPARATOR);
-            uri.append(fragment.get());
+            return new Uri( input.concat(suffix) );
         }
-        return Uri.cast(uri.toString());
     }
 
-    /// NEW END
+    /**
+     * Returns a new Uri instance w/ the given new query {@code newQuery}.
+     *
+     * @throws URISyntaxException if this Uri is {@link #opaque}
+     *             or if the new string {@code uri} doesn't fit to the
+     *             specification RFC2396 and RFC3986 or could not be parsed correctly.
+     */
+    public final Uri getNewQuery(final Encoded newQuery) throws URISyntaxException {
+        if( opaque ) {
+            throw new URISyntaxException(input.decode(), "Opaque Uri cannot permute by query");
+        } else {
+            // with host validation if authority is defined
+            return Uri.create(scheme, userInfo, host, port, path, newQuery, fragment);
+        }
+    }
 
     /**
      * {@inheritDoc}
diff --git a/src/java/com/jogamp/common/net/UriQueryProps.java b/src/java/com/jogamp/common/net/UriQueryProps.java
index 8d9bcb4..a93a1eb 100644
--- a/src/java/com/jogamp/common/net/UriQueryProps.java
+++ b/src/java/com/jogamp/common/net/UriQueryProps.java
@@ -63,14 +63,14 @@ public class UriQueryProps {
    public final Map<String, String> getProperties() { return properties; }
    public final char getQuerySeparator() { return query_separator.charAt(0); }
 
-   public final String appendQuery(String baseQuery) {
+   public final Uri.Encoded appendQuery(Uri.Encoded baseQuery) {
        boolean needsSep = false;
        final StringBuilder sb = new StringBuilder();
        if ( null != baseQuery ) {
            if( baseQuery.startsWith(QMARK) ) {
                baseQuery = baseQuery.substring(1); // cut off '?'
            }
-           sb.append(baseQuery);
+           sb.append(baseQuery.get());
            if( !baseQuery.endsWith(query_separator) ) {
                needsSep = true;
            }
@@ -87,11 +87,11 @@ public class UriQueryProps {
            }
            needsSep = true;
        }
-       return sb.toString();
+       return new Uri.Encoded(sb.toString(), Uri.QUERY_LEGAL);
    }
 
    public final Uri appendQuery(final Uri base) throws URISyntaxException {
-       return base.getNewQuery( appendQuery( Uri.decode(base.query) ) );
+       return base.getNewQuery( appendQuery( base.query ) );
    }
 
    /**
-- 
cgit v1.2.3