Limit path media type mappings in Jersey
Asked Answered
S

1

3

I have configured MEDIA_TYPE_MAPPINGS for my Jersey apps. Unfortunately, this causes some trouble with a generic upload service in my app.

@PUT
@Path("files/{filename}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response uploadFile(
    @PathParam("filename") @NotNull @Size(max = 240) String filename, DataSource dataSource)

If someone uploads .../files/file.xml the extension is chopped of.

Is there a way to tell Jersey to skip that filtering for this resource?

Edit: After peeskillet's answer, my assumption was confirmed. I have filed an improvement request: https://java.net/jira/browse/JERSEY-2780

Sacking answered 6/2, 2015 at 15:42 Comment(0)
O
5

First of all, this is in no way a bug. It is the expected behavior. The purpose of media type mappings is not related to working with files, but instead an alternative form of content negotiation for cases where setting headers may not be available, for instance in a browser.

Though not in the official spec, this feature was part of a draft prior to the specs final release. Most implementations decided to include it one way or another. Jersey happens to let you configure it. So can see here in the spec in 3.7.1 Request Preprocessing

  1. Set
  • M = {config.getMediaTypeMappings().keySet()}
  • L = {config.getLanguageMappings().keySet()}
  • m = null
  • l = null
  • Where config is an instance of the application-supplied subclass of ApplicationConfig.
  1. For each extension (a . character followed by one or more alphanumeric characters) e in the final path segment scanning from right to left:
  • (a) Remove the leading ‘.’ character from e
  • (b) If m is null and e is a member of M then remove the corresponding extension from the effective request URI and set m = e.
  • (c) Else if l is null and e is a member of L then remove the corresponding extension from the effective request URI and set l = e. Else go to step 4
  1. If m is not null then set the value of the Accept header to config.getExtensionMappings().get(m)

3(b) is basically saying that the extension should be removed from the requested URI and 4 is stating that there should be some extension mapping that would map say json (the extension) to application/json and set that as the Accept header. You can see from different tests, this behavior

@POST
@Path("/files/{file}")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response doTest(@PathParam("file") String fileName, @Context HttpHeaders headers) {
    String accept = headers.getHeaderString(HttpHeaders.ACCEPT);
    return Response.ok(fileName + "; Accept: " + accept).build();
}
...

Map<String, MediaType> map = new HashMap<>();
map.put("xml", MediaType.APPLICATION_XML_TYPE);
resourceCnfig.property(ServerProperties.MEDIA_TYPE_MAPPINGS, map);

curl -v http://localhost:8080/api/mapping/files/file.xml -X POST
Result: file; Accept: application/xml

If we comment out that configuration property, you will see that the Accept header hasn't been set.

curl -v http://localhost:8080/api/mapping/files/file.xml -X POST
Result: file.xml; Accept: */**

That being said...

When you configure the ServerProperties.MEDIA_TYPE_MAPPINGS, the org.glassfish.jersey.server.filter.UriConnegFilter is the filter used for this feature. You can see in the source code in line 162 and 179, where the filter is stripping the extension

path = new StringBuilder(path).delete(index, index + suffix.length() + 1).toString();
...
rc.setRequestUri(uriInfo.getRequestUriBuilder().replacePath(path).build(new Object[0]));

So there's no way to configure this (at least as far as I can tell from looking at the source), so we would have to extend that class, override the filter method and take out, at minimum, that last line that actually does the replacing, then register the filter. Here's what I did to get it to work. I simply copy and pasted the code from the filter, and commented out the line where it replaces the extension

import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.annotation.Priority;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.server.filter.UriConnegFilter;

@PreMatching
@Priority(3000)
public class MyUriConnegFilter extends UriConnegFilter {

    public MyUriConnegFilter(@Context Configuration config) {
        super(config);
    }
    
    public MyUriConnegFilter(Map<String, MediaType> mediaTypeMappings, 
                             Map<String, String> languageMappings) {
        super(mediaTypeMappings, languageMappings);
    }

    @Override
    public void filter(ContainerRequestContext rc)
            throws IOException {
        UriInfo uriInfo = rc.getUriInfo();

        String path = uriInfo.getRequestUri().getRawPath();
        if (path.indexOf('.') == -1) {
            return;
        }
        List<PathSegment> l = uriInfo.getPathSegments(false);
        if (l.isEmpty()) {
            return;
        }
        PathSegment segment = null;
        for (int i = l.size() - 1; i >= 0; i--) {
            segment = (PathSegment) l.get(i);
            if (segment.getPath().length() > 0) {
                break;
            }
        }
        if (segment == null) {
            return;
        }
        int length = path.length();

        String[] suffixes = segment.getPath().split("\\.");
        for (int i = suffixes.length - 1; i >= 1; i--) {
            String suffix = suffixes[i];
            if (suffix.length() != 0) {
                MediaType accept = (MediaType) this.mediaTypeMappings.get(suffix);
                if (accept != null) {
                    rc.getHeaders().putSingle("Accept", accept.toString());

                    int index = path.lastIndexOf('.' + suffix);
                    path = new StringBuilder(path).delete(index, index + suffix.length() + 1).toString();
                    suffixes[i] = "";
                    break;
                }
            }
        }
        for (int i = suffixes.length - 1; i >= 1; i--) {
            String suffix = suffixes[i];
            if (suffix.length() != 0) {
                String acceptLanguage = (String) this.languageMappings.get(suffix);
                if (acceptLanguage != null) {
                    rc.getHeaders().putSingle("Accept-Language", acceptLanguage);

                    int index = path.lastIndexOf('.' + suffix);
                    path = new StringBuilder(path).delete(index, index + suffix.length() + 1).toString();
                    suffixes[i] = "";
                    break;
                }
            }
        }
        if (length != path.length()) {
            //rc.setRequestUri(uriInfo.getRequestUriBuilder().replacePath(path).build(new Object[0]));
        }
    }
}

Then configure it

Map<String, MediaType> map = new HashMap<>();
map.put("xml", MediaType.APPLICATION_XML_TYPE);
map.put("json", MediaType.APPLICATION_JSON_TYPE);
resourceConfig.register(new MyUriConnegFilter(map, null));

curl -v http://localhost:8080/api/mapping/files/file.xml -X POST
Result: file.xml; Accept: application/xml

curl -v http://localhost:8080/api/mapping/files/file.json -X POST
Result: file.json; Accept: application/json

Obtrusive answered 9/2, 2015 at 14:45 Comment(3)
I am aware that this isn't bug. Good analysis, though. I checked the filter first and my review required the same as you did. The only thing I could do is file an improvement request. This is a functional limitation.Sacking
So I reread your question, and you want to just skip that one resource. I'm curious though what would be the determining factor? I could see the use of an annotation, but that would require another filter, as the one that is currently used is a pre-matching filter, so we can't yet get the resource info. A header is possible, but that doesn't seem very elegant and the client would have to know/remember to set this header.Obtrusive
I although had some annotation and preprocesing in mind. Though, this is a lot of fuzz. Piping a through a temporary header isn't ideal, I don't want the client to change anything. This is purely a server-side problem.Sacking

© 2022 - 2024 — McMap. All rights reserved.