Возврат 406 для медиатипа пользовательской версии в Spring Boot

У меня есть несколько классов в Spring Boot (1.5.2), чтобы включить управление версиями с помощью настраиваемого тега mediatype. Медиатип имеет формат application/vnd.<application>.<version>+json.

ApiVersionedResource,

@RequestMapping
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersionedResource {

    /**
     * Media type without the version for e.g. application/vnd.orchestral.onboarding
     *
     * @return
     */
    String media() default Constants.DEFAULT_API_MEDIA_TYPE;

    /**
     * Version of the API as defined by {@link Version}
     *
     * @return
     */
    String version() default Constants.DEFAULT_API_VERSION;
}

ApiVersionedResourceRequestCondition,

public class ApiVersionedResourceRequestCondition extends AbstractRequestCondition<ApiVersionedResourceRequestCondition> {

    private final static Logger log = LoggerFactory.getLogger(ApiVersionedResourceRequestCondition.class);

    private final Set<Version> versions;
    private final String acceptedMediaType;
    private final static Version MAX_API_VERSION = new Version(Constants.MAX_API_VERSION);

    public ApiVersionedResourceRequestCondition(String acceptedMediaType, String version) {
        this(acceptedMediaType, versionGenerator(version));
    }

    public ApiVersionedResourceRequestCondition(String acceptedMediaType, Collection<Version> versions) {
        this.acceptedMediaType = acceptedMediaType;
        this.versions = Collections.unmodifiableSet(new HashSet<>(versions));
    }

    @Override
    public ApiVersionedResourceRequestCondition combine(ApiVersionedResourceRequestCondition other) {
        log.debug("Combining:\n{}\n{}", this, other);
        Set<Version> newVersions = new LinkedHashSet<>(this.versions);
        newVersions.addAll(other.versions);
        String newMediaType;
        if (StringUtils.hasText(this.acceptedMediaType) && StringUtils.hasText(other.acceptedMediaType)
                && !this.acceptedMediaType.equals(other.acceptedMediaType)) {
            throw new IllegalArgumentException(String.format("Both conditions should have the same media type however its %s != %s",
                    this.acceptedMediaType, other.acceptedMediaType));
        } else if (StringUtils.hasText(this.acceptedMediaType)) {
            newMediaType = this.acceptedMediaType;
        } else {
            newMediaType = other.acceptedMediaType;
        }
        return new ApiVersionedResourceRequestCondition(newMediaType, newVersions);
    }

    @Override
    public ApiVersionedResourceRequestCondition getMatchingCondition(HttpServletRequest request) {
        final String accept = request.getHeader("Accept");
        log.debug("Accept header = {}", accept);
        if (StringUtils.hasText(accept)) {
            final Pattern regexPattern = Pattern.compile("(.*)\\.(\\d+\\.\\d+).*");
            final Matcher matcher = regexPattern.matcher(accept);
            if (matcher.matches()) {
                final String actualMediaType = matcher.group(1);
                final Version version = new Version(matcher.group(2));
                log.debug("Version={}", version);

                if (acceptedMediaType.startsWith(actualMediaType)) {
                    for (Version definedVersion : versions) {
                        if (definedVersion.compareTo(version) == 0) {
                            return this;
                        }
                    }
                    log.debug("Unable to find a matching version");
                } else {
                    log.debug("Unable to find a valid media type {}", acceptedMediaType);
                }
            }
        }
        return null;
    }

    @Override
    protected Collection<?> getContent() {
        return versions;
    }

    @Override
    protected String getToStringInfix() {
        return " && ";
    }

    @Override
    public int compareTo(ApiVersionedResourceRequestCondition other, HttpServletRequest request) {
        return 0;
    }

    /**
     * Converts a given version string to {@link Version}
     *
     * @param commaDelimitedVersioning
     * @return
     */
    private static Set<Version> versionGenerator(final String commaDelimitedVersioning) {
        HashSet<Version> versionRanges = new HashSet<>();

        if (StringUtils.hasText(commaDelimitedVersioning)) {
            final String[] versions = StringUtils.tokenizeToStringArray(commaDelimitedVersioning.trim(), ",");
            Arrays.stream(versions).forEach(i -> {
                Version v = new Version(i);
                if (v.compareTo(MAX_API_VERSION) > 1) {
                    throw new IllegalArgumentException(
                            String.format("Specified version %s is highest than the max version %s", v, MAX_API_VERSION));
                }
                versionRanges.add(v);
            });
        }

        return versionRanges;
    }
}

Веб-конфигурация,

@Configuration
@ConditionalOnClass({ ApiVersionedResource.class })
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(true)
                .favorParameter(false)
                .ignoreAcceptHeader(false)
                .useJaf(false)
                .defaultContentType(MediaType.APPLICATION_JSON)
                .mediaType("json", MediaType.APPLICATION_JSON);
    }

    @Bean
    public WebMvcRegistrationsAdapter webMvcRegistrationsHandlerMapping() {
        return new WebMvcRegistrationsAdapter() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new CustomRequestMappingHandlerMapping();
            }
        };
    }

    private static final class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

        @Override
        protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
            ApiVersionedResource typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersionedResource.class);
            return createCondition(typeAnnotation);
        }

        @Override
        protected RequestCondition<?> getCustomMethodCondition(Method method) {
            ApiVersionedResource methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersionedResource.class);
            return createCondition(methodAnnotation);
        }

        @Override
        protected boolean isHandler(Class<?> beanType) {
            return super.isHandler(beanType) && (AnnotationUtils.findAnnotation(beanType, ApiVersionedResource.class) != null);
        }

        private RequestCondition<?> createCondition(ApiVersionedResource versionMapping) {
            if (versionMapping != null) {
                return new ApiVersionedResourceRequestCondition(versionMapping.media(), versionMapping.version());
            }

            return null;
        }
    }
}

В моем ResourceController я установил аннотацию как,

@RestController
@RequestMapping(Constants.DEFAULT_API_PREFIX)
@ApiVersionedResource
@Api(description = "Operations to onboard Organizations",
        tags = {"organizations"},
        produces = "application/json")
public class OrganizationResource {
    .....
}

Если я укажу заголовок Accept как неизвестный тип носителя, например application/vnd.test.blah+json, я хочу, чтобы сервер возвращал клиенту ошибку 406.

К сожалению, это возвращает ошибку 404.

Что мне нужно исправить, чтобы вернуть ошибку 406?


person nixgadget    schedule 16.03.2017    source источник
comment
404 относится к NOT_FOUND, почему?   -  person zakaria amine    schedule 16.03.2017
comment
Да, знаю. Я не уверен, почему это выдает 404 вместо 406 неприемлемо. Ясно, что я здесь что-то не так делаю.   -  person nixgadget    schedule 16.03.2017


Ответы (1)


Вы можете добавить совет контроллеру:

    @ControllerAdvice
public class MediaNotSupportedExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = { HttpMediaTypeNotSupportedException.class })
    protected ResponseEntity<Void> handleMediaNotSupported(RuntimeException ex, WebRequest request) {

    return new ResponseEntity<Void>(HttpStatus.NOT_ACCEPTABLE);
}

Вместе с перехватчиком:

public class MediaInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {

        if(request.getHeader("Accept") == null || compare against your desired media type)
        throw new HttpMediaTypeNotSupportedException();
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }
}

Однако брошенный 404 сбивает с толку.

person zakaria amine    schedule 16.03.2017
comment
Да я мог. Но ему нужно куда-нибудь выбросить HttpMediaTypeNotSupportedException, чтобы этот совет контроллера сработал. - person nixgadget; 16.03.2017
comment
или добавить фильтр, а затем выбросить исключение внутри фильтра - person zakaria amine; 16.03.2017
comment
когда вы имеете в виду фильтр, вы имеете в виду перехватчик? - person nixgadget; 16.03.2017
comment
Фильтр или Inteceptor могут выполнять ту же работу. Я отредактировал свой ответ. - person zakaria amine; 16.03.2017