com.github.jmnarloch.spring.cloud.feign.VndErrorDecoder.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jmnarloch.spring.cloud.feign.VndErrorDecoder.java

Source

/**
 * Copyright (c) 2015 the original author or authors
 * <p/>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "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 com.github.jmnarloch.spring.cloud.feign;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import feign.FeignException;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.VndErrors;
import org.springframework.hateoas.VndErrors.VndError;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

/**
 * A a custom error decoder capable of instantiating {@link VndErrorException}. The decoder will try to match any
 * responses matching the {@code application/vnd.error+json} content types and unmarshall the {@link VndErrors}
 * instance. If that fails it will make a second attempt to retrieve single {@link VndError} out of the response body.
 * Afterwards the unmarshalled error object will wrapped into {@link VndErrorException} and propagated by Feign.
 *
 * @author Jakub Narloch
 * @see VndErrors
 * @see VndErrorException
 * @see <a href="https://github.com/blongden/vnd.error">https://github.com/blongden/vnd.error</a>
 */
public class VndErrorDecoder implements ErrorDecoder, InitializingBean {

    /**
     * Logger instance used by this class.
     */
    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /**
     * The content type header.
     */
    private static final String CONTENT_TYPE_HEADER = "Content-Type";

    /**
     * The expected JSON vnd.error media type.
     */
    private static final String JSON_VND_ERROR_MEDIA_TYPE = "application/vnd.error+json";

    /**
     * The optional instance of the Jackson {@link ObjectMapper}, if non has been configured a new instance
     * will be created.
     */
    @Autowired(required = false)
    private ObjectMapper objectMapper;

    /**
     * Initializes all needed properties.
     *
     * @throws Exception if any error occurs
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        if (objectMapper == null) {
            objectMapper = new ObjectMapper();
        }
    }

    /**
     * Decodes the JSON vnd.error out of the response payload, if case that no matching content type has been found
     * fallbacks to the default decoder.
     *
     * @param methodKey the method key
     * @param response  the response object
     * @return the decoded exception
     */
    @Override
    public Exception decode(String methodKey, Response response) {

        try {
            if (hasVndError(response)) {
                return decodeVndError(response);
            }

            return new ErrorDecoder.Default().decode(methodKey, response);
        } catch (IOException e) {
            logger.error("An unexpected error occurred during vnd.error decoding", e);
            throw FeignException.errorStatus(methodKey, response);
        }
    }

    /**
     * Returns true if the response contains vnd.error as the body payload.
     *
     * @param response the response object
     * @return {@code true} if response contains vnd.error, {@code false} otherwise
     */
    private boolean hasVndError(Response response) {
        Collection<String> contentTypes = response.headers().get(CONTENT_TYPE_HEADER);
        return contentTypes != null && contains(contentTypes, JSON_VND_ERROR_MEDIA_TYPE);
    }

    /**
     * Decodes the vnd.error out of the response body.
     *
     * @return the decoded exception
     * @throws IOException if any error occurs during response processing
     */
    @SuppressWarnings("PMD.EmptyCatchBlock")
    private Exception decodeVndError(Response response) throws IOException {

        VndErrors vndErrors = null;
        final byte[] body = body(response);

        try {
            vndErrors = reader(VndErrors.class).readValue(body);
        } catch (JsonProcessingException e) {
            // ignores exception
        }

        if (vndErrors == null) {
            final VndError vndError = reader(VndError.class).readValue(body);
            vndErrors = new VndErrors(vndError);
        }

        return createException(response, body, vndErrors);
    }

    /**
     * Returns the object reader for specified type.
     *
     * @param expectedType the type to unmarshall
     * @return the object reader for specified type
     */
    private ObjectReader reader(Class<?> expectedType) {

        return objectMapper.reader(expectedType);
    }

    /**
     * Reads the entire response body content and returns it as byte array.
     *
     * @param response the response object
     * @return the body content
     * @throws IOException if any error occurs during response processing
     */
    private static byte[] body(Response response) throws IOException {
        try (Response.Body body = response.body()) {
            return IOUtils.toByteArray(body.asInputStream());
        }
    }

    /**
     * Returns whether within the specified collection any string contains a specific substring.
     *
     * @param collection the collections of strings
     * @param pattern    the pattern to search
     * @return {@code true} if any entry in collections contains a {@code pattern}, {@code false} otherwise
     */
    private static boolean contains(Collection<String> collection, String pattern) {
        for (String entry : collection) {
            if (entry.contains(pattern)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Creates the instance of {@link VndErrorException}.
     *
     * @param response  the response
     * @param body      the response body
     * @param vndErrors the vnd errors  @return the exception instance
     */
    private VndErrorException createException(Response response, byte[] body, VndErrors vndErrors) {

        final HttpStatus status = HttpStatus.valueOf(response.status());
        final HttpHeaders headers = mapHeaders(response.headers());
        final Charset charset = getCharset(headers);
        return new VndErrorException(status, status.getReasonPhrase(), headers, body, charset, vndErrors);
    }

    /**
     * Maps the response headers map to {@link HttpHeaders}.
     *
     * @param responseHeaders the response headers
     * @return the http headers
     */
    private HttpHeaders mapHeaders(Map<String, Collection<String>> responseHeaders) {
        final HttpHeaders headers = new HttpHeaders();
        for (Map.Entry<String, Collection<String>> header : responseHeaders.entrySet()) {
            headers.put(header.getKey(), new ArrayList<>(header.getValue()));
        }
        return headers;
    }

    /**
     * Retrieves the response charset.
     * @param headers the http headers
     * @return the response charset
     */
    private Charset getCharset(HttpHeaders headers) {
        final MediaType contentType = headers.getContentType();
        return contentType != null ? contentType.getCharSet() : null;
    }
}