@Nonnull private ValidationReport validatePathParameters(final ApiOperation apiOperation) { ValidationReport validationReport = empty(); final NormalisedPath requestPath = apiOperation.getRequestPath(); for (int i = 0; i < apiOperation.getApiPath().numberOfParts(); i++) { if (!apiOperation.getApiPath().hasParams(i)) { continue; } final ValidationReport pathPartValidation = apiOperation .getApiPath() .paramValues(i, requestPath.part(i)) .entrySet() .stream() .map(param -> validatePathParameter(apiOperation, param.getKey(), param.getValue())) .reduce(empty(), ValidationReport::merge); validationReport = validationReport.merge(pathPartValidation); } return validationReport; }
/** * Matches all operations with the given method. Both request and response errors can be matched with this. */ public static WhitelistRule methodIs(final PathItem.HttpMethod method) { return new PrintableWhitelistRule( "Method is " + method, (message, operation, request, response) -> operation != null && operation.getMethod() == method); }
@Override public boolean matches(final Message message, final ApiOperation operation, final Request request) { if (operation == null || operation.getOperation().getRequestBody() == null) { return false; } final RequestBody apiRequestBody = operation.getOperation().getRequestBody(); // TODO: This should really respect the content-type of the response to filter schemas return apiRequestBody.getContent().values().stream() .map(MediaType::getSchema) .filter(Objects::nonNull) .map(Schema::get$ref) .filter(Objects::nonNull) .anyMatch($ref -> $ref.endsWith("/" + entityName)); }
/** * Matches operations that contain the given regular expression in their API path. * <p> * The tested path does not have parameters materialized, but is taken from the API * definition, e.g. "/store/order/{orderId}". */ public static WhitelistRule pathContains(final String regexp) { return new PrintableWhitelistRule( "Api path contains: '" + regexp + "'", (message, operation, request, response) -> operation != null && regexpContain(operation.getApiPath().normalised(), regexp)); }
return ValidationReport.singleton( messages.get("validation.response.body.missing", apiOperation.getMethod(), apiOperation.getApiPath().original()) );
/** * Validate the request against the given API operation * * @param request The request to validate * @param apiOperation The operation to validate the request against * * @return A validation report containing validation errors */ @Nonnull public ValidationReport validateRequest(final Request request, final ApiOperation apiOperation) { requireNonNull(request, "A request is required"); requireNonNull(apiOperation, "An API operation is required"); final MessageContext context = MessageContext.create() .in(REQUEST) .withApiOperation(apiOperation) .withRequestPath(apiOperation.getRequestPath().original()) .withRequestMethod(request.getMethod()) .build(); return securityValidator.validateSecurity(request, apiOperation) .merge(validateContentType(request, apiOperation)) .merge(validateAccepts(request, apiOperation)) .merge(validateHeaders(request, apiOperation)) .merge(validatePathParameters(apiOperation)) .merge(requestBodyValidator.validateRequestBody(request, apiOperation.getOperation().getRequestBody())) .merge(validateQueryParameters(request, apiOperation)) .withAdditionalContext(context); }
/** * Tries to find the best fitting API path matching the given path and request method. * * @param path the requests path to find in API definition * @param method the {@link Request.Method} for the request * @return a {@link ApiOperationMatch} containing the information if the path is defined, the operation * is allowed and having the necessary {@link ApiOperation} if applicable */ @Nonnull public ApiOperationMatch findApiOperation(final String path, final Request.Method method) { // try to find possible matching paths regardless of HTTP method final NormalisedPath requestPath = new NormalisedPathImpl(path, apiPrefix); final List<ApiPath> matchingPaths = apiPathsGroupedByNumberOfParts .getOrDefault(requestPath.numberOfParts(), emptyList()).stream() .filter(p -> p.matches(requestPath)) .collect(toList()); if (matchingPaths.isEmpty()) { return ApiOperationMatch.MISSING_PATH; } // try to find the operation which fits the HTTP method, // choosing the most 'specific' path match from the candidates final PathItem.HttpMethod httpMethod = PathItem.HttpMethod.valueOf(method.name()); final Optional<ApiPath> matchingPathAndOperation = matchingPaths.stream() .filter(apiPath -> operations.contains(apiPath.original(), httpMethod)) .max(comparingInt(ApiOperationResolver::specificityScore)); return matchingPathAndOperation .map(match -> new ApiOperationMatch(new ApiOperation(match, requestPath, httpMethod, operations.get(match.original(), httpMethod)))) .orElse(ApiOperationMatch.NOT_ALLOWED_OPERATION); }
@Nonnull private ValidationReport validateHeader(final ApiOperation apiOperation, final String headerName, final Header apiHeader, final Collection<String> propertyValues) { if (propertyValues.isEmpty() && TRUE.equals(apiHeader.getRequired())) { return ValidationReport.singleton( messages.get("validation.response.header.missing", headerName, apiOperation.getApiPath().original()) ); } return propertyValues .stream() .map(v -> schemaValidator.validate(v, apiHeader.getSchema(), "response.header")) .reduce(ValidationReport.empty(), ValidationReport::merge); } }
public Optional<OpenApiOperation> findApiOperation(String method, String path) { String relativePath = UrlUtils.extractPath(path); Request.Method requestMethod = Enum.valueOf(Request.Method.class, method); ApiOperationMatch apiOperation = apiOperationResolver.findApiOperation(relativePath, requestMethod); if (! apiOperation.isPathFound() || ! apiOperation.isOperationAllowed()) { return Optional.empty(); } return Optional.of(new OpenApiOperation(method, combineWithBasePath(apiOperation.getApiOperation().getApiPath().original()))); }
@Nonnull private ValidationReport validatePathParameter(final ApiOperation apiOperation, final String paramName, final Optional<String> paramValue) { return defaultIfNull(apiOperation.getOperation().getParameters(), Collections.<Parameter>emptyList()) .stream() .filter(RequestValidator::isPathParam) .filter(p -> p.getName().equalsIgnoreCase(paramName)) .findFirst() .map(p -> parameterValidator.validate(paramValue.orElse(null), p)) .orElse(empty()); }
@Nonnull private ValidationReport validateParameter(final ApiOperation apiOperation, final Parameter parameter, final Collection<String> parameterValues, final String missingKey) { final ValidationReport.MessageContext context = ValidationReport.MessageContext.create().withParameter(parameter).build(); if (parameterValues.isEmpty() && TRUE.equals(parameter.getRequired())) { return ValidationReport.singleton( messages.get(missingKey, parameter.getName(), apiOperation.getApiPath().original()) ).withAdditionalContext(context); } return parameterValues .stream() .map(v -> parameterValidator.validate(v, parameter)) .reduce(empty(), ValidationReport::merge); }
@Nonnull private Collection<String> getProduces(final ApiOperation apiOperation) { return apiOperation.getOperation() .getResponses() .values() .stream() .filter(apiResponse -> apiResponse.getContent() != null) .flatMap(apiResponse -> apiResponse.getContent().keySet().stream()) .collect(Collectors.toSet()); }
return ValidationReport.singleton( messages.get("validation.response.status.unknown", response.getStatus(), apiOperation.getApiPath().original()) ).withAdditionalContext(contextBuilder.build());
@Nullable private ApiResponse getApiResponse(final Response response, final ApiOperation apiOperation) { final ApiResponse apiResponse = apiOperation.getOperation().getResponses().get(Integer.toString(response.getStatus())); if (apiResponse == null) { return apiOperation.getOperation().getResponses().getDefault(); } return apiResponse; } }
@Nonnull private Collection<String> getConsumes(final ApiOperation apiOperation) { if (apiOperation.getOperation().getRequestBody() == null) { return emptyList(); } return defaultIfNull(apiOperation.getOperation().getRequestBody().getContent().keySet(), emptySet()); }
@Override public boolean matches(final Message message, final ApiOperation operation, final Response response) { if (operation == null || operation.getOperation().getResponses() == null) { return false; } final ApiResponse apiResponse = getApiResponse(response, operation); if (apiResponse == null) { return false; } // TODO: This should really respect the content-type of the response to filter schemas return apiResponse.getContent().values().stream() .map(MediaType::getSchema) .filter(Objects::nonNull) .map(Schema::get$ref) .filter(Objects::nonNull) .anyMatch($ref -> $ref.endsWith("/" + entityName)); }
@Nullable private ApiResponse getApiResponse(final Response response, final ApiOperation apiOperation) { final ApiResponse apiResponse = apiOperation.getOperation().getResponses().get(Integer.toString(response.getStatus())); if (apiResponse == null) { return apiOperation.getOperation().getResponses().get("default"); // try the default response } return apiResponse; }
@Nonnull private ValidationReport validateQueryParameters(final Request request, final ApiOperation apiOperation) { return defaultIfNull(apiOperation.getOperation().getParameters(), Collections.<Parameter>emptyList()) .stream() .filter(RequestValidator::isQueryParam) .map(p -> validateParameter( apiOperation, p, request.getQueryParameterValues(p.getName()), "validation.request.parameter.query.missing") ) .reduce(empty(), ValidationReport::merge); }
@Nonnull private ValidationReport validateHeaders(final Request request, final ApiOperation apiOperation) { return defaultIfNull(apiOperation.getOperation().getParameters(), Collections.<Parameter>emptyList()) .stream() .filter(RequestValidator::isHeaderParam) .map(p -> validateParameter( apiOperation, p, request.getHeaderValues(p.getName()), "validation.request.parameter.header.missing") ) .reduce(empty(), ValidationReport::merge); }
@Nonnull public ValidationReport validateSecurity(final Request request, final ApiOperation apiOperation) { final List<SecurityRequirement> securityRequired = apiOperation.getOperation().getSecurity();