What is the Jersey 2 replacement for ResourceConte

2020-08-13 07:35发布

问题:

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

回答1:

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)



回答2:

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);
}


回答3:

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;
    }
}