You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2019/06/23 00:08:13 UTC

[juneau] branch master updated: JUNEAU-97 Path Parameter in Parent RestResource

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 3b9f323  JUNEAU-97 Path Parameter in Parent RestResource
3b9f323 is described below

commit 3b9f323f776ae9a2ce3573d0d55f9f01425b6e00
Author: JamesBognar <ja...@apache.org>
AuthorDate: Sat Jun 22 20:07:07 2019 -0400

    JUNEAU-97 Path Parameter in Parent RestResource
---
 .../juneau/rest/util/UrlPathPatternTest.java       |  9 +--
 .../apache/juneau/rest/BasicRestCallHandler.java   | 74 ++++++++++------------
 .../{util/UrlPathParts.java => Constants.java}     | 62 ++----------------
 .../java/org/apache/juneau/rest/RequestPath.java   |  6 ++
 .../org/apache/juneau/rest/RestCallRouter.java     |  4 +-
 .../java/org/apache/juneau/rest/RestContext.java   | 21 ++++--
 .../org/apache/juneau/rest/RestMethodContext.java  |  2 +-
 .../util/{UrlPathParts.java => UrlPathInfo.java}   | 54 +++++++++-------
 .../apache/juneau/rest/util/UrlPathPattern.java    | 32 +++++-----
 .../juneau/rest/util/UrlPathPatternMatch.java      | 64 +++++++++++++++++--
 10 files changed, 169 insertions(+), 159 deletions(-)

diff --git a/juneau-rest/juneau-rest-server-test/src/test/java/org/apache/juneau/rest/util/UrlPathPatternTest.java b/juneau-rest/juneau-rest-server-test/src/test/java/org/apache/juneau/rest/util/UrlPathPatternTest.java
index 014b813..5498f2d 100644
--- a/juneau-rest/juneau-rest-server-test/src/test/java/org/apache/juneau/rest/util/UrlPathPatternTest.java
+++ b/juneau-rest/juneau-rest-server-test/src/test/java/org/apache/juneau/rest/util/UrlPathPatternTest.java
@@ -58,7 +58,7 @@ public class UrlPathPatternTest {
 		l.add(new UrlPathPattern("/foo/{id}/bar/*"));
 
 		Collections.sort(l);
-		assertEquals("['/foo/bar','/foo/bar/*','/foo/{id}/bar','/foo/{id}/bar/*','/foo/{id}','/foo/{id}/*','/foo','/foo/*','/','/*','','*']", SimpleJsonSerializer.DEFAULT.toString(l));
+		assertEquals("['/foo/bar','/foo/bar/*','/foo/{id}/bar','/foo/{id}/bar/*','/foo/{id}','/foo/{id}/*','/foo','/foo/*','/','/*','/','/*']", SimpleJsonSerializer.DEFAULT.toString(l));
 	}
 
 	@Test
@@ -79,7 +79,7 @@ public class UrlPathPatternTest {
 		l.add(new UrlPathPattern(""));
 
 		Collections.sort(l);
-		assertEquals("['/foo/bar','/foo/bar/*','/foo/{id}/bar','/foo/{id}/bar/*','/foo/{id}','/foo/{id}/*','/foo','/foo/*','/','/*','','*']", SimpleJsonSerializer.DEFAULT.toString(l));
+		assertEquals("['/foo/bar','/foo/bar/*','/foo/{id}/bar','/foo/{id}/bar/*','/foo/{id}','/foo/{id}/*','/foo','/foo/*','/','/*','/','/*']", SimpleJsonSerializer.DEFAULT.toString(l));
 	}
 
 	@Test
@@ -118,7 +118,7 @@ public class UrlPathPatternTest {
 	public void b03_simple_match_2parts() throws Exception {
 		UrlPathPattern p = new UrlPathPattern("/foo/bar");
 		shouldMatch(p, "/foo/bar", "{}");
-		shouldMatch(p, "foo/bar/", "{r:''}");
+		shouldMatch(p, "/foo/bar/", "{r:''}");
 	}
 
 	@Test
@@ -134,7 +134,6 @@ public class UrlPathPatternTest {
 	public void b05_simple_match_0parts() throws Exception {
 		UrlPathPattern p = new UrlPathPattern("/");
 		shouldMatch(p, "/", "{r:''}");
-		shouldMatch(p, "", "{}");
 	}
 
 	@Test
@@ -148,7 +147,6 @@ public class UrlPathPatternTest {
 	public void b07_simple_match_blank() throws Exception {
 		UrlPathPattern p = new UrlPathPattern("");
 		shouldMatch(p, "/", "{r:''}");
-		shouldMatch(p, "", "{}");
 	}
 
 	@Test
@@ -186,7 +184,6 @@ public class UrlPathPatternTest {
 	public void c03_simple_withRemainder_match_2parts() throws Exception {
 		UrlPathPattern p = new UrlPathPattern("/foo/bar/*");
 		shouldMatch(p, "/foo/bar", "{}");
-		shouldMatch(p, "foo/bar/", "{r:''}");
 		shouldMatch(p, "/foo/bar/baz", "{r:'baz'}");
 		shouldMatch(p, "/foo/bar/baz/", "{r:'baz/'}");
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
index 1a591d1..91a96e2 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.rest.Constants.*;
 import static java.util.logging.Level.*;
 import static javax.servlet.http.HttpServletResponse.*;
 import static org.apache.juneau.internal.IOUtils.*;
@@ -27,7 +28,7 @@ import javax.servlet.http.*;
 import org.apache.juneau.http.StreamResource;
 import org.apache.juneau.rest.RestContext.*;
 import org.apache.juneau.rest.exception.*;
-import org.apache.juneau.rest.util.RestUtils;
+import org.apache.juneau.rest.util.*;
 
 /**
  * Default implementation of {@link RestCallHandler}.
@@ -112,49 +113,38 @@ public class BasicRestCallHandler implements RestCallHandler {
 			context.checkForInitException();
 
 			String pathInfo = RestUtils.getPathInfoUndecoded(r1);  // Can't use r1.getPathInfo() because we don't want '%2F' resolved.
+			UrlPathInfo upi = new UrlPathInfo(pathInfo);
 
 			// If this resource has child resources, try to recursively call them.
 			if (pathInfo != null && context.hasChildResources() && (! pathInfo.equals("/"))) {
-//				for (Map.Entry<UrlPathPattern,RestContext> e : context.getChildResources().entrySet()) {
-//					UrlPathPattern upp = e.getKey();
-//					String[] vars = upp.match(pathInfo);
-//					if (vars != null) {
-//						for (int i = 0; i < vars.length; i++)
-//							r1.setAttribute(upp.getVars()[i], vars[i]);
-//						final String pathInfoRemainder = upp.(i == -1 ? null : pathInfo.substring(i));
-//						final String servletPath = r1.getServletPath() + "/" + pathInfoPart;
-//						final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) {
-//							@Override /* ServletRequest */
-//							public String getPathInfo() {
-//								return urlDecode(pathInfoRemainder);
-//							}
-//							@Override /* ServletRequest */
-//							public String getServletPath() {
-//								return servletPath;
-//							}
-//						};
-//						childResource.getCallHandler().service(childRequest, r2);
-//						return;
-//					}
-//				}
-				int i = pathInfo.indexOf('/', 1);
-				String pathInfoPart = i == -1 ? pathInfo.substring(1) : pathInfo.substring(1, i);
-				RestContext childResource = context.getChildResource(pathInfoPart);
-				if (childResource != null) {
-					final String pathInfoRemainder = (i == -1 ? null : pathInfo.substring(i));
-					final String servletPath = r1.getServletPath() + "/" + pathInfoPart;
-					final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) {
-						@Override /* ServletRequest */
-						public String getPathInfo() {
-							return urlDecode(pathInfoRemainder);
+				for (RestContext rc : context.getChildResources().values()) {
+					UrlPathPattern upp = rc.pathPattern;
+					final UrlPathPatternMatch uppm = upp.match(upi);
+					if (uppm != null) {
+						if (uppm.hasVars()) {
+							@SuppressWarnings("unchecked")
+							Map<String,String> vars = (Map<String,String>)r1.getAttribute(REST_PATHVARS_ATTR);
+							if (vars == null) {
+								vars = new TreeMap<>();
+								r1.setAttribute(REST_PATHVARS_ATTR, vars);
+							}
+							vars.putAll(uppm.getVars());
 						}
-						@Override /* ServletRequest */
-						public String getServletPath() {
-							return servletPath;
-						}
-					};
-					childResource.getCallHandler().service(childRequest, r2);
-					return;
+						final String afterMatch = uppm.getSuffix();
+						final String servletPath = r1.getServletPath() + uppm.getPrefix();
+						final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) {
+							@Override /* ServletRequest */
+							public String getPathInfo() {
+								return urlDecode(afterMatch);
+							}
+							@Override /* ServletRequest */
+							public String getServletPath() {
+								return servletPath;
+							}
+						};
+						rc.getCallHandler().service(childRequest, r2);
+						return;
+					}
 				}
 			}
 
@@ -187,9 +177,9 @@ public class BasicRestCallHandler implements RestCallHandler {
 				// If the specified method has been defined in a subclass, invoke it.
 				int rc = SC_METHOD_NOT_ALLOWED;
 				if (restCallRouters.containsKey(methodUC)) {
-					rc = restCallRouters.get(methodUC).invoke(pathInfo, req, res);
+					rc = restCallRouters.get(methodUC).invoke(upi, req, res);
 				} else if (restCallRouters.containsKey("*")) {
-					rc = restCallRouters.get("*").invoke(pathInfo, req, res);
+					rc = restCallRouters.get("*").invoke(upi, req, res);
 				}
 
 				// If not invoked above, see if it's an OPTIONs request
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/Constants.java
similarity index 54%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java
copy to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/Constants.java
index 4ca7a15..9158710 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/Constants.java
@@ -10,67 +10,15 @@
 // * "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.juneau.rest.util;
-
-import static org.apache.juneau.internal.StringUtils.*;
+package org.apache.juneau.rest;
 
 /**
- * Represents a parsed URL path.
+ * Constant strings.
  */
-public class UrlPathParts {
-
-	final String[] parts;
-	final String raw;
-
-	/**
-	 * Constructor.
-	 *
-	 * @param path The path.
-	 */
-	public UrlPathParts(String path) {
-		path = emptyIfNull(path);
-		raw = path;
-		if (path.length() > 0 && path.charAt(0) == '/')
-			path = path.substring(1);
-		parts = split(path, '/');
-		for (int i = 0; i < parts.length; i++)
-			parts[i] = urlDecode(parts[i]);
-	}
-
-	/**
-	 * Returns the path parts.
-	 *
-	 * @return The path parts.
-	 */
-	public String[] getParts() {
-		return parts;
-	}
-
-	/**
-	 * Returns a path remainder given the specified number of prefix parts.
-	 *
-	 * @param i The number of prefix parts to discard.
-	 * @return The remainder.
-	 */
-	public String getRemainder(int i) {
-		String s = raw;
-		if (s.length() > 0 && s.charAt(0) == '/')
-			s = s.substring(1);
-		for (int j = 0; j < s.length(); j++) {
-			if (i == 0)
-				return s.substring(j);
-			if (i > 0 && s.charAt(j) == '/')
-				i--;
-		}
-		return isTrailingSlash() ? "" : null;
-	}
+class Constants {
 
 	/**
-	 * Returns <jk>true</jk> if this path ends with a slash.
-	 *
-	 * @return <jk>true</jk> if this path ends with a slash.
+	 * Request attribute name for passing path variables from parent to child.
 	 */
-	public boolean isTrailingSlash() {
-		return raw.endsWith("/");
-	}
+	static final String REST_PATHVARS_ATTR = "juneau.pathVars";
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
index 44e5d7c..8705e09 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.rest.Constants.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
@@ -43,6 +44,11 @@ public class RequestPath extends TreeMap<String,String> {
 	RequestPath(RestRequest req) {
 		super(String.CASE_INSENSITIVE_ORDER);
 		this.req = req;
+		@SuppressWarnings("unchecked")
+		Map<String,String> parentVars = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR);
+		if (parentVars != null)
+			for (Map.Entry<String,String> e : parentVars.entrySet())
+				put(e.getKey(), e.getValue());
 	}
 
 	RequestPath parser(HttpPartParser parser) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
index 8a09ba1..57e333b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
@@ -18,6 +18,8 @@ import java.util.*;
 
 import javax.servlet.http.*;
 
+import org.apache.juneau.rest.util.*;
+
 /**
  * Represents a group of CallMethods on a REST resource that handle the same HTTP Method name but with different
  * paths/matchers/guards/etc...
@@ -75,7 +77,7 @@ public class RestCallRouter {
 	 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta)
 	 * @return The HTTP response code.
 	 */
-	int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable {
+	int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {
 		if (restJavaMethods.length == 1)
 			return restJavaMethods[0].invoke(pathInfo, req, res);
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 03b805c..d62fb9a 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -64,7 +64,7 @@ import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.converters.*;
 import org.apache.juneau.rest.exception.*;
 import org.apache.juneau.rest.reshandlers.*;
-import org.apache.juneau.rest.util.UrlPathPattern;
+import org.apache.juneau.rest.util.*;
 import org.apache.juneau.rest.vars.*;
 import org.apache.juneau.rest.widget.*;
 import org.apache.juneau.serializer.*;
@@ -3230,6 +3230,7 @@ public final class RestContext extends BeanContext {
 		uriAuthority,
 		uriContext;
 	final String fullPath;
+	final UrlPathPattern pathPattern;
 
 	private final Set<String> allowedMethodParams;
 
@@ -3458,7 +3459,12 @@ public final class RestContext extends BeanContext {
 					msgs.addSearchPath(mbl[i] != null ? mbl[i].baseClass : resourceClass, mbl[i].bundlePath);
 			}
 
-			fullPath = (builder.parentContext == null ? "" : (builder.parentContext.fullPath + '/')) + builder.getPath();
+			this.fullPath = (builder.parentContext == null ? "" : (builder.parentContext.fullPath + '/')) + builder.getPath();
+			
+			String p = builder.getPath();
+			if (! p.endsWith("/*"))
+				p += "/*";
+			this.pathPattern = new UrlPathPattern(p);
 
 			this.childResources = Collections.synchronizedMap(new LinkedHashMap<String,RestContext>());  // Not unmodifiable on purpose so that children can be replaced.
 
@@ -3513,7 +3519,7 @@ public final class RestContext extends BeanContext {
 							sm = new RestMethodContext(smb) {
 
 								@Override
-								int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable {
+								int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {
 
 									int rc = super.invoke(pathInfo, req, res);
 									if (rc != SC_OK)
@@ -3526,10 +3532,11 @@ public final class RestContext extends BeanContext {
 										return SC_OK;
 
 									} else if ("POST".equals(req.getMethod())) {
-										if (pathInfo.indexOf('/') != -1)
-											pathInfo = pathInfo.substring(pathInfo.lastIndexOf('/')+1);
-										pathInfo = urlDecode(pathInfo);
-										RemoteInterfaceMethod rmm = rim.getMethodMetaByPath(pathInfo);
+										String pip = pathInfo.getPath();
+										if (pip.indexOf('/') != -1)
+											pip = pip.substring(pip.lastIndexOf('/')+1);
+										pip = urlDecode(pip);
+										RemoteInterfaceMethod rmm = rim.getMethodMetaByPath(pip);
 										if (rmm != null) {
 											Method m = rmm.getJavaMethod();
 											try {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
index e0a1425..d500808 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
@@ -738,7 +738,7 @@ public class RestMethodContext extends BeanContext implements Comparable<RestMet
 	 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta)
 	 * @return The HTTP response code.
 	 */
-	int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable {
+	int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {
 
 		UrlPathPatternMatch pm = pathPattern.match(pathInfo);
 		if (pm == null)
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathInfo.java
similarity index 68%
rename from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java
rename to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathInfo.java
index 4ca7a15..2e79a1d 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathParts.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathInfo.java
@@ -14,25 +14,27 @@ package org.apache.juneau.rest.util;
 
 import static org.apache.juneau.internal.StringUtils.*;
 
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.*;
+
 /**
- * Represents a parsed URL path.
+ * Represents a parsed URL path-info string.
  */
-public class UrlPathParts {
+public class UrlPathInfo {
 
 	final String[] parts;
-	final String raw;
+	final String path;
 
 	/**
 	 * Constructor.
 	 *
 	 * @param path The path.
 	 */
-	public UrlPathParts(String path) {
-		path = emptyIfNull(path);
-		raw = path;
-		if (path.length() > 0 && path.charAt(0) == '/')
-			path = path.substring(1);
-		parts = split(path, '/');
+	public UrlPathInfo(String path) {
+		if (path != null && ! path.startsWith("/"))
+			throw new RuntimeException("Invalid path specified.  Must be null or start with '/' per HttpServletRequest.getPathInfo().");
+		this.path = path;
+		parts = path == null ? new String[0] : split(path.substring(1), '/');
 		for (int i = 0; i < parts.length; i++)
 			parts[i] = urlDecode(parts[i]);
 	}
@@ -47,22 +49,12 @@ public class UrlPathParts {
 	}
 
 	/**
-	 * Returns a path remainder given the specified number of prefix parts.
+	 * Returns the raw path passed into this object.
 	 *
-	 * @param i The number of prefix parts to discard.
-	 * @return The remainder.
+	 * @return The raw path passed into this object.
 	 */
-	public String getRemainder(int i) {
-		String s = raw;
-		if (s.length() > 0 && s.charAt(0) == '/')
-			s = s.substring(1);
-		for (int j = 0; j < s.length(); j++) {
-			if (i == 0)
-				return s.substring(j);
-			if (i > 0 && s.charAt(j) == '/')
-				i--;
-		}
-		return isTrailingSlash() ? "" : null;
+	public String getPath() {
+		return path;
 	}
 
 	/**
@@ -71,6 +63,20 @@ public class UrlPathParts {
 	 * @return <jk>true</jk> if this path ends with a slash.
 	 */
 	public boolean isTrailingSlash() {
-		return raw.endsWith("/");
+		return path.endsWith("/");
+	}
+
+	/**
+	 * Converts this object to a map.
+	 *
+	 * @return This object converted to a map.
+	 */
+	public ObjectMap toMap() {
+		return new DefaultFilteringObjectMap().append("raw", path).append("parts", parts);
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return SimpleJson.DEFAULT.toString(toMap());
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPattern.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPattern.java
index 3507136..c486b7b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPattern.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPattern.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.util;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.util.*;
 import java.util.regex.*;
 
@@ -39,7 +41,7 @@ public final class UrlPathPattern implements Comparable<UrlPathPattern> {
 	 * @param patternString The raw pattern string from the {@link RestMethod#path() @RestMethod(path)} annotation.
 	 */
 	public UrlPathPattern(String patternString) {
-		this.pattern = patternString;
+		this.pattern = isEmpty(patternString) ? "/" : patternString.charAt(0) != '/' ? '/' + patternString : patternString;
 
 		String c = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W");
 		if (c.isEmpty())
@@ -48,7 +50,7 @@ public final class UrlPathPattern implements Comparable<UrlPathPattern> {
 			c = c + "/W";
 		this.comparator = c;
 
-		String[] parts = new UrlPathParts(patternString).getParts();
+		String[] parts = new UrlPathInfo(pattern).getParts();
 
 		this.hasRemainder = parts.length > 0 && "*".equals(parts[parts.length-1]);
 
@@ -77,36 +79,36 @@ public final class UrlPathPattern implements Comparable<UrlPathPattern> {
 	 * 	A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
 	 */
 	public UrlPathPatternMatch match(String path) {
-		return match(new UrlPathParts(path));
+		return match(new UrlPathInfo(path));
 	}
 
 	/**
 	 * Returns a non-<jk>null</jk> value if the specified path matches this pattern.
 	 *
-	 * @param path The path to match against.
+	 * @param pathInfo The path to match against.
 	 * @return
 	 * 	A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
 	 */
-	public UrlPathPatternMatch match(UrlPathParts path) {
+	public UrlPathPatternMatch match(UrlPathInfo pathInfo) {
 
-		String[] pp = path.getParts();
+		String[] pip = pathInfo.getParts();
 
-		if (parts.length != pp.length) {
+		if (parts.length != pip.length) {
 			if (hasRemainder) {
-				if (pp.length == parts.length - 1 && ! path.isTrailingSlash())
+				if (pip.length == parts.length - 1 && ! pathInfo.isTrailingSlash())
 					return null;
-				else if (pp.length < parts.length)
+				else if (pip.length < parts.length)
 					return null;
 			} else {
-				if (pp.length != parts.length + 1)
+				if (pip.length != parts.length + 1)
 					return null;
-				if (! path.isTrailingSlash())
+				if (! pathInfo.isTrailingSlash())
 					return null;
 			}
 		}
 
 		for (int i = 0; i < parts.length; i++)
-			if (vars[i] == null && (pp.length <= i || ! ("*".equals(parts[i]) || pp[i].equals(parts[i]))))
+			if (vars[i] == null && (pip.length <= i || ! ("*".equals(parts[i]) || pip[i].equals(parts[i]))))
 				return null;
 
 		String[] vals = varKeys == null ? null : new String[varKeys.length];
@@ -115,11 +117,9 @@ public final class UrlPathPattern implements Comparable<UrlPathPattern> {
 		if (vals != null)
 			for (int i = 0; i < parts.length; i++)
 				if (vars[i] != null)
-					vals[j++] = pp[i];
-
-		String remainder = path.getRemainder(parts.length);
+					vals[j++] = pip[i];
 
-		return new UrlPathPatternMatch(varKeys, vals, remainder);
+		return new UrlPathPatternMatch(pathInfo.getPath(), parts.length, varKeys, vals);
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPatternMatch.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPatternMatch.java
index fa39d42..f16b56f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPatternMatch.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/util/UrlPathPatternMatch.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.util;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -26,18 +28,21 @@ import org.apache.juneau.marshall.*;
  */
 public class UrlPathPatternMatch {
 
-	private final String remainder;
+	private final int matchedParts;
+	private final String path;
 	private final Map<String,String> vars;
 
 	/**
 	 * Constructor.
 	 *
+	 * @param path The path being matched against.  Can be <jk>null</jk>.
+	 * @param matchedParts The number of parts that were matched against the path.
 	 * @param keys The variable keys.  Can be <jk>null</jk>.
 	 * @param values The variable values.  Can be <jk>null</jk>.
-	 * @param remainder
 	 */
-	protected UrlPathPatternMatch(String[] keys, String[] values, String remainder) {
-		this.remainder = remainder;
+	protected UrlPathPatternMatch(String path, int matchedParts, String[] keys, String[] values) {
+		this.path = path;
+		this.matchedParts = matchedParts;
 		this.vars = keys == null ? Collections.emptyMap() : new SimpleMap<>(keys, values);
 	}
 
@@ -51,12 +56,61 @@ public class UrlPathPatternMatch {
 	}
 
 	/**
+	 * Returns <jk>true</jk> if this match contains one or more variables.
+	 *
+	 * @return <jk>true</jk> if this match contains one or more variables.
+	 */
+	public boolean hasVars() {
+		return ! vars.isEmpty();
+	}
+
+	/**
 	 * Returns the remainder of the path after the pattern match has been made.
 	 *
 	 * @return The remainder of the path after the pattern match has been made.
 	 */
 	public String getRemainder() {
-		return remainder;
+		String suffix = getSuffix();
+		if (isNotEmpty(suffix) && suffix.charAt(0) == '/')
+			suffix = suffix.substring(1);
+		return suffix;
+	}
+
+	/**
+	 * Returns the remainder of the URL after the pattern was matched.
+	 *
+	 * @return
+	 * The remainder of the URL after the pattern was matched.
+	 * <br>Can be <jk>null</jk> if nothing remains to be matched.
+	 * <br>Otherwise, always starts with <js>'/'</js>.
+	 */
+	public String getSuffix() {
+		String s = path;
+		for (int j = 0; j < matchedParts; j++) {
+			int k = s.indexOf('/', 1);
+			if (k == -1)
+				return null;
+			s = s.substring(k);
+		}
+		return s;
+	}
+
+	/**
+	 * Returns the part of the URL that the pattern matched against.
+	 *
+	 * @return
+	 * The part of the URL that the pattern matched against.
+	 * <br>Can be <jk>null</jk> if nothing matched.
+	 * <br>Otherwise, always starts with <js>'/'</js>.
+	 */
+	public String getPrefix() {
+		int c = 0;
+		for (int j = 0; j < matchedParts; j++) {
+			c = path.indexOf('/', c+1);
+			if (c == -1)
+				c = path.length();
+		}
+		return nullIfEmpty(path.substring(0, c));
 	}
 
 	/**