All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.atlassian.oai.validator.springmvc.OpenApiValidationService Maven / Gradle / Ivy

There is a newer version: 2.43.0
Show newest version
package com.atlassian.oai.validator.springmvc;

import com.atlassian.oai.validator.OpenApiInteractionValidator;
import com.atlassian.oai.validator.model.Body;
import com.atlassian.oai.validator.model.Request;
import com.atlassian.oai.validator.model.Response;
import com.atlassian.oai.validator.model.SimpleRequest;
import com.atlassian.oai.validator.model.SimpleResponse;
import com.atlassian.oai.validator.report.ValidationReport;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.Objects.requireNonNull;

public class OpenApiValidationService {

    private static final List URI_CHARSETS = resolveAvailableCharsets();
    private final OpenApiInteractionValidator validator;
    private final UrlPathHelper urlPathHelper;

    public OpenApiValidationService(final EncodedResource specAsResource, final UrlPathHelper urlPathHelper) throws IOException {
        this(OpenApiInteractionValidator
                .createForInlineApiSpecification(IOUtils.toString(specAsResource.getReader()))
                .withLevelResolver(SpringMVCLevelResolverFactory.create())
                .build(), urlPathHelper);
    }

    public OpenApiValidationService(final OpenApiInteractionValidator validator, final UrlPathHelper urlPathHelper) {
        requireNonNull(validator, "An OpenAPI validator is required.");
        this.validator = validator;
        this.urlPathHelper = urlPathHelper;
    }

    private static List resolveAvailableCharsets() {
        final List uriCharsets = Charset.availableCharsets().values().stream()
                .map(Charset::name)
                .collect(Collectors.toList());
        // put UTF-8 to the front as it is the most probable charset for the URI encoding
        uriCharsets.remove(StandardCharsets.UTF_8.name());
        uriCharsets.add(0, StandardCharsets.UTF_8.name());
        return uriCharsets;
    }

    private String resolveServletPath(final HttpServletRequest servletRequest) {
        // The method HttpServletRequest#getServletPath might return NULL even in case there is an actual
        // servlet path. The UrlPathHelper is helping getting the servlet path.
        return urlPathHelper.getPathWithinApplication(servletRequest);
    }

    /**
     * @param servletRequest the {@link HttpServletRequest}
     * @param bodySupplier the {@link Supplier} of a {@link Body} for the request validation
     *
     * @return the build {@link Request} created out of given {@link HttpServletRequest}
     */
    public Request buildRequest(final HttpServletRequest servletRequest, final Supplier bodySupplier) {
        requireNonNull(servletRequest, "A request is required.");
        requireNonNull(bodySupplier, "A body supplier is required.");

        final Request.Method method = Request.Method.valueOf(servletRequest.getMethod());
        final String path = resolveServletPath(servletRequest);
        final SimpleRequest.Builder builder = new SimpleRequest.Builder(method, path);
        // As a precaution read the query parameters before (!!) the body. The ContentCachingRequestWrapper,
        // used for form data validation, parses the parameters from the bodies input stream. They would be
        // indistinguishable from the form data.
        final Set queryParameterNames = getQueryParameterNames(servletRequest);
        builder.withBody(bodySupplier.get());
        for (final String queryParameterName : queryParameterNames) {
            builder.withQueryParam(queryParameterName, servletRequest.getParameterValues(queryParameterName));
        }
        for (final String headerName : Collections.list(servletRequest.getHeaderNames())) {
            builder.withHeader(headerName, Collections.list(servletRequest.getHeaders(headerName)));
        }
        return builder.build();
    }

    /**
     * @param servletResponse the {@link javax.servlet.http.HttpServletResponse} whose body is cached
     *
     * @return the build {@link Response} created out of given {@link ContentCachingResponseWrapper}
     */
    public Response buildResponse(final ContentCachingResponseWrapper servletResponse) {
        final int statusCode = servletResponse.getStatusCode();
        final SimpleResponse.Builder builder =
                new SimpleResponse.Builder(statusCode)
                        .withBody(servletResponse.getContentAsByteArray())
                        .withContentType(servletResponse.getContentType());
        for (final String headerName : servletResponse.getHeaderNames()) {
            builder.withHeader(headerName, newArrayList(servletResponse.getHeaders(headerName)));
        }

        return builder.build();
    }

    /**
     * @param request the {@link Request} to validate against the OpenAPI / Swagger specification
     *
     * @return the {@link ValidationReport} for the validated {@link Request}
     */
    public ValidationReport validateRequest(final Request request) {
        return validator.validateRequest(request);
    }

    /**
     * @param servletRequest the {@link HttpServletRequest} to examine the api path to validate against
     * @param response the {@link Response} to validate against the OpenAPI / Swagger specification
     *
     * @return the {@link ValidationReport} for the validated {@link Request}
     */
    public ValidationReport validateResponse(final HttpServletRequest servletRequest,
                                             final Response response) {
        final Request.Method method = Request.Method.valueOf(servletRequest.getMethod());
        final String path = resolveServletPath(servletRequest);
        return validator.validateResponse(path, method, response);
    }

    /**
     * @param servletResponse the {@link HttpServletResponse}
     *
     * @return a map of the headers that are currently set on the response
     */
    Map> resolveHeadersOnResponse(final HttpServletResponse servletResponse) {
        final Map> headers = new HashMap<>();
        final Collection headerNames = servletResponse.getHeaderNames();
        if (headerNames != null) {
            for (final String headerName : headerNames) {
                headers.put(headerName, newArrayList(servletResponse.getHeaders(headerName)));
            }
        }
        return Collections.unmodifiableMap(headers);
    }

    /**
     * Will set the given headers on the given response.
     *
     * @param servletResponse the {@link HttpServletResponse}
     * @param headers the headers to add to the response
     */
    void addHeadersToResponse(final HttpServletResponse servletResponse, final Map> headers) {
        if (headers != null) {
            for (final Map.Entry> header : headers.entrySet()) {
                final String name = header.getKey();
                for (final String value : header.getValue()) {
                    servletResponse.addHeader(name, value);
                }
            }
        }
    }

    private static Set getQueryParameterNames(final HttpServletRequest servletRequest) {
        final List allAvailableNames = Collections.list(servletRequest.getParameterNames());
        return Stream.of(StringUtils.split(StringUtils.defaultIfBlank(servletRequest.getQueryString(), ""), "&"))
                .map(str -> uriDecodeParamName(allAvailableNames, StringUtils.split(str, "=")[0]))
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toSet());
    }

    private static String uriDecodeParamName(final List allParameterNames, final String paramName) {
        // It is difficult to get the correct uri encoding. Ideally it does not need decoding. And if it's encoded better
        // with UTF-8 as charset.
        // Beyond that it's guessing. It can be verified by checking the decoded name with all available parameter names.
        if (!allParameterNames.contains(paramName)) {
            for (final String charset : URI_CHARSETS) {
                final String decoded = uriDecode(paramName, charset);
                if (allParameterNames.contains(decoded)) {
                    return decoded;
                }
            }
        }
        return paramName;
    }

    private static String uriDecode(final String paramName, final String charset) {
        try {
            return URLDecoder.decode(paramName, charset);
        } catch (final UnsupportedEncodingException e) {
            // should not happen as only supported charsets will be used
            return paramName;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy