How do I find out what Resource an arbitrary URI maps to under Jersey 2.0? Under Jersey 1.x I'd use ResourceContext.matchResource(URI).
What I'm trying to do: I'm trying to process an incoming request that references another resource by URI. For example, here is an operation that links a user to a department.
POST /departments/5
{
"user": "http://example.com/users/10"
}
POST /departments/5 resolves to:
class DepartmentResource
{
@POST
void linkUser() { ... }
}
In order to honor this request, I need to resolve the URI back to a UserResource and from there to its database id. Adding @Context UriInfo
to linkUser()
won't help because this UriInfo
corresponds to the URI of the department instead of the user.
UPDATE: I filed a bug report at https://github.com/eclipse-ee4j/jersey/issues/2444
UPDATE2: Posted a follow-up question: Jersey2: Navigating from a Resource to an instance
If you are running in a container, you can do the following:
// Classes that may be of interest
import org.glassfish.jersey.server.ExtendedResourceContext;
import org.glassfish.jersey.server.model.Invocable;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
@Context private ExtendedResourceContext rconfig;
public Resource dumpResources(String relUrl){
List<Resource> parents = rconfig.getResourceModel().getRootResources();
for(Resource res: parents){
if(relUrl.startsWith(res.getPath()){
return res;
}
}
}
If you have a more complex hierarchy with children resources, you can either recursively descend into their children with the following method, or you can create a ResourceModelVisitor and use the visitor pattern via:
rconfig.getResourceModel().accept( (ResourceModelVisitor) visitor);
There may be another solution, but I can verify this works. I use this pattern to dynamically document my resources (instead of wadl)
Answering my own question:
import com.google.common.primitives.Ints;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.PathParam;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.bitbucket.cowwoc.preconditions.Preconditions;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.server.ExtendedResourceContext;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.uri.PathPattern;
import org.glassfish.jersey.uri.UriComponent;
import org.glassfish.jersey.uri.UriTemplate;
[...]
/**
* Returns the resource associated with a URI.
* <p/>
* WARNING: The input URI must be encoded according to the rule of {@link Path#value}.
* <p/>
* @param <T> the resource type
* @param uri the encoded URI
* @param type the resource type to return
* @return the resource associated with the URI
* @throws NullPointerException if uri or type are null
* @throws IllegalArgumentException if the URI does not map to this resource type
* @throws ProcessingException if an exception occurs while creating the resource
*/
public <T> T getByUri(URI uri, Class<T> type)
throws NullPointerException, IllegalArgumentException, ProcessingException
{
Preconditions.requireThat(uri, "uri").isNotNull();
Preconditions.requireThat(type, "type").isNotNull();
UriInfo uriInfo = serviceLocator.getService(UriInfo.class);
ExtendedResourceContext resourceContext = serviceLocator.
getService(ExtendedResourceContext.class);
if (uriInfo.getBaseUri().relativize(uri).normalize().getPath().startsWith(".."))
{
throw new IllegalArgumentException("The uri did not originate from this server: " + uri +
". The expected base URI is: " + uriInfo.getBaseUri());
}
String path = uri.getRawPath();
Resource match = findMatchingResource(resourceContext.getResourceModel().getRootResources(),
path);
if (match == null)
throw new IllegalArgumentException(uri + " did not match any registered resource");
Class<?> matchedType = getType(match);
if (!type.isAssignableFrom(matchedType))
{
throw new IllegalArgumentException(uri + " is not of type " + type + ". It is of type: " +
matchedType);
}
@SuppressWarnings("unchecked")
T result = (T) createResource(match, path);
return result;
}
/**
* Returns the resource or sub-resource that matches a path segment.
* <p>
* @param resources a list of candidate resources
* @param path a path segment
* @return null if no match is found
* @throws NullPointerException if resources or path are null
*/
private Resource findMatchingResource(List<Resource> resources, String path)
throws NullPointerException
{
for (Resource resource: resources)
{
PathPattern pattern = resource.getPathPattern();
MatchResult matcher = pattern.match(path);
if (matcher == null)
continue;
assert (matcher.groupCount() >= 1): "resource: " + resource + ", path: " +
resource.getPath() + ", match: " + matcher;
// The segment not consumed by the parent resource
String remaining = matcher.group(matcher.groupCount());
if (remaining != null)
remaining = remaining.trim();
if (remaining == null || remaining.isEmpty() || remaining.equals("/"))
return resource;
Resource descendant = findMatchingResource(resource.getChildResources(), remaining);
if (descendant != null)
return descendant;
}
return null;
}
/**
* @param resource a resource
* @return the user class the resource references
* @throws NullPointerException resource is null
*/
private Class<?> getType(Resource resource) throws NullPointerException
{
try
{
ResourceMethod resourceLocator = resource.getResourceLocator();
if (resourceLocator != null)
return (Class<?>) resourceLocator.getInvocable().getResponseType();
for (String name: resource.getNames())
{
// ASSUMPTION: Merged resource models contain only one JAX-RS resource class.
// Other resource models (e.g. corresponding to subresource methods) are named: "[unnamed]".
if (name.isEmpty() || !Character.isAlphabetic(name.charAt(0)))
continue;
return Class.forName(name, true, getClass().getClassLoader());
}
throw new AssertionError("Resources must have at least one name: " + resource);
}
catch (ClassNotFoundException e)
{
// ASSUMPTION: The metadata cannot exist unless the class was already loaded by the ClassLoader.
throw new AssertionError(e);
}
}
/**
* Creates the resource associated with a URI path.
* <p>
* @param resource the resource type
* @param path the URI path
* @return the resource
* @throws NullPointerException if resource or path are null
* @throws IllegalArgumentException if the URI does not map to this resource type
* @throws ProcessingException if an exception occurs while creating the resource
*/
private Object createResource(Resource resource, String path)
throws NullPointerException, IllegalArgumentException, ProcessingException
{
List<Resource> resourceTree = getAncestorTree(resource);
// Process the root of the tree
Resource rootResource = resourceTree.get(0);
Object parentResource = serviceLocator.getService(getType(rootResource));
if (resourceTree.size() == 1)
return parentResource;
// The path segment not matched by the root
String remaining = getRemainingPath(rootResource, path);
// Process the rest of the tree
for (Resource currentResource: resourceTree.subList(1, resourceTree.size()))
{
List<String> stringArguments = getStringArguments(currentResource, remaining);
remaining = getRemainingPath(currentResource, remaining);
Method resourceLocator = currentResource.getResourceLocator().getInvocable().
getHandlingMethod();
// Convert the resource locator arguments from String to the expected type
Class<?>[] parameterTypes = resourceLocator.getParameterTypes();
Object[] arguments = new Object[parameterTypes.length];
for (int i = 0, size = stringArguments.size(); i < size; ++i)
{
Class<?> expectedType = parameterTypes[i];
arguments[i] = parse(stringArguments.get(i), expectedType);
}
// Invoke the resource locator
try
{
parentResource = resourceLocator.invoke(parentResource, arguments);
}
catch (ReflectiveOperationException e)
{
throw new ProcessingException(e);
}
}
return parentResource;
}
/**
* @param resource a resource
* @param path a path segment matched by the resource
* @return the path segment not consumed by the resource
*/
private String getRemainingPath(Resource resource, String path)
{
MatchResult matcher = resource.getPathPattern().match(path);
assert (matcher.groupCount() >= 1): "resource: " + resource + ", path: " + resource.getPath() +
", match: " + matcher;
// The remaining path (not consumed by the parent resource)
return matcher.group(matcher.groupCount());
}
/**
* @param method a resource locator
* @param path the path segment that the method matched
* @return a list of the string arguments passed into the method
*/
private List<String> getStringArguments(Resource resource, String path)
{
// Parse the URI into template key-value pairs
MatchResult match = resource.getPathPattern().match(path);
List<String> templateKeys = resource.getPathPattern().getTemplate().getTemplateVariables();
Map<String, String> template = new HashMap<>((int) Math.ceil(templateKeys.size() / 0.75));
List<Integer> groupNumbers = getGroupNumbers(resource.getPathPattern());
for (int i = 0, size = templateKeys.size(); i < size; ++i)
template.put(templateKeys.get(i), match.group(groupNumbers.get(i)));
Method resourceLocator = resource.getResourceLocator().getInvocable().getHandlingMethod();
List<String> result = new ArrayList<>(resourceLocator.getParameters().length);
for (Parameter parameter: resourceLocator.getParameters())
{
PathParam annotation = parameter.getAnnotation(PathParam.class);
if (annotation == null)
continue;
String key = annotation.value();
String value = template.get(key);
result.add(UriComponent.decode(value, UriComponent.Type.PATH));
}
return result;
}
/**
* @param pattern a PathPattern
* @return the group numbers of the top-level capturing groups
*/
private List<Integer> getGroupNumbers(PathPattern pattern)
{
int[] candidate = pattern.getGroupIndexes();
if (candidate.length > 0)
return Ints.asList((int[]) candidate);
UriTemplate template = pattern.getTemplate();
List<Integer> result = new ArrayList<>(template.getTemplateVariables().size());
String regex = template.getPattern().getRegex();
// Find an unescaped open or close brace
Matcher braces = Pattern.compile("[^\\\\](\\(|\\))").matcher(regex);
int level = 0;
int groupNumber = 1;
while (braces.find())
{
switch (braces.group(1))
{
case "(":
{
if (level == 0)
result.add(groupNumber);
++groupNumber;
++level;
break;
}
case ")":
{
--level;
assert (level >= 0): regex;
break;
}
default:
throw new AssertionError("Unexpected character: " + braces.group(1));
}
}
return result;
}
/**
* @param resource a resource
* @return a list of resources, from the top-most ancestor down to the resource itself
* @throws NullPointerException resource is null
*/
private List<Resource> getAncestorTree(Resource resource)
throws NullPointerException
{
List<Resource> result = new ArrayList<>(10);
Resource current = resource;
do
{
result.add(current);
current = current.getParent();
}
while (current != null);
Collections.reverse(result);
return result;
}
/**
* Converts a String to a different type.
* <p>
* @param value the string value
* @param type the desired type
* @return the parsed value
*/
private Object parse(String value, Class<?> type)
{
// PERFORMANCE: Because the small number of entries, it's faster to do a linear search than
// using a HashMap. Testing under Java 8.
if (type.isAssignableFrom(String.class))
return value;
if (type.isAssignableFrom(boolean.class) || type.isAssignableFrom(Boolean.class))
return Boolean.valueOf(value);
if (type.isAssignableFrom(char.class) || type.isAssignableFrom(Character.class))
{
if (value.length() != 0)
throw new ProcessingException("Expecting a single character. Got: \"" + value + "\"");
return value.charAt(0);
}
if (type.isAssignableFrom(byte.class) || type.isAssignableFrom(Byte.class))
return Byte.valueOf(value);
if (type.isAssignableFrom(short.class) || type.isAssignableFrom(Short.class))
return Short.valueOf(value);
if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class))
return Integer.valueOf(value);
if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class))
return Long.valueOf(value);
if (type.isAssignableFrom(float.class) || type.isAssignableFrom(Float.class))
return Float.valueOf(value);
if (type.isAssignableFrom(double.class) || type.isAssignableFrom(Double.class))
return Double.valueOf(value);
throw new AssertionError("Unexpected type: " + type);
}
Where do you want to use it? If I'm right, you can use the context variable, a UriInfo instance to get the Matched Resources:
@Path("books")
public class BookResource {
private static final Logger LOGGER = Logger.getLogger(BookResource.class);
@Context UriInfo uriInfo;
@Path("{bookId:[0-9]*}")
@GET
@Produces({ MediaType.APPLICATION_XML })
public Book getBookByPath(@PathParam("bookId") final Long bookId,@Context final Application application) {
final Book book = new Book(bookId);
BookResource.LOGGER.debug(book);
List<Object> matchedResources= uriInfo.getMatchedResources();
for (Object matchedResource : matchedResources) {
LOGGER.debug(matchedResource.getClass());
}
LOGGER.debug(uriInfo.getAbsolutePath());
return book;
}
}