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

guru.nidi.ramltester.core.RamlChecker Maven / Gradle / Ivy

/*
 * Copyright (C) 2014 Stefan Niederhauser ([email protected])
 *
 * 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
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 guru.nidi.ramltester.core;

import guru.nidi.ramltester.model.RamlMessage;
import guru.nidi.ramltester.model.RamlRequest;
import guru.nidi.ramltester.model.RamlResponse;
import guru.nidi.ramltester.model.Values;
import guru.nidi.ramltester.util.FormDecoder;
import guru.nidi.ramltester.util.InvalidMediaTypeException;
import guru.nidi.ramltester.util.MediaType;
import guru.nidi.ramltester.util.UriComponents;
import org.raml.model.*;
import org.raml.model.parameter.AbstractParam;
import org.raml.model.parameter.UriParameter;

import java.io.UnsupportedEncodingException;
import java.util.*;

import static guru.nidi.ramltester.core.UsageBuilder.*;

/**
 *
 */
public class RamlChecker {
    private final static class DefaultHeaders {
        private static final Set
                REQUEST = new HashSet<>(Arrays.asList("accept", "accept-charset", "accept-encoding", "accept-language", "accept-datetime", "authorization", "cache-control", "connection", "cookie", "content-length", "content-md5", "content-type", "date", "dnt", "expect", "from", "host", "if-match", "if-modified-since", "if-none-match", "if-range", "if-unmodified-since", "max-forwards", "origin", "pragma", "proxy-authorization", "range", "referer", "te", "user-agent", "upgrade", "via", "warning")),
                RESPONSE = new HashSet<>(Arrays.asList("access-control-allow-origin", "accept-ranges", "age", "allow", "cache-control", "connection", "content-encoding", "content-language", "content-length", "content-location", "content-md5", "content-disposition", "content-range", "content-type", "date", "etag", "expires", "last-modified", "link", "location", "p3p", "pragma", "proxy-authenticate", "refresh", "retry-after", "server", "set-cookie", "status", "strict-transport-security", "trailer", "transfer-encoding", "upgrade", "vary", "via", "warning", "www-authenticate", "x-frame-options"));
    }

    private final Raml raml;
    private final List schemaValidators;
    private final String baseUri;
    private final boolean ignoreXheaders;
    private RamlViolations requestViolations, responseViolations;
    private Usage usage;

    public RamlChecker(Raml raml, List schemaValidators, String baseUri, boolean ignoreXheaders) {
        this.raml = raml;
        this.schemaValidators = schemaValidators;
        this.baseUri = baseUri;
        this.ignoreXheaders = ignoreXheaders;
    }

    public RamlReport check(RamlRequest request, RamlResponse response) {
        RamlReport report = new RamlReport(raml);
        usage = report.getUsage();
        requestViolations = report.getRequestViolations();
        responseViolations = report.getResponseViolations();
        try {
            Action action = checkRequestAndFindAction(request);
            if (response != null) {
                checkResponse(action, response);
            }
        } catch (RamlViolationException e) {
            //ignore, results are in report
        }
        return report;
    }

    public RamlReport check(RamlRequest request) {
        return check(request, null);
    }

    public Action checkRequestAndFindAction(RamlRequest request) {
        final UriComponents requestUri = UriComponents.fromHttpUrl(request.getRequestUrl(baseUri));
        final UriComponents ramlUri = UriComponents.fromHttpUrl(raml.getBaseUri());

        final VariableMatcher hostMatch = getHostMatch(requestUri, ramlUri);
        final VariableMatcher pathMatch = getPathMatch(requestUri, ramlUri);

        Resource resource = findResource(pathMatch.getSuffix());
        resourceUsage(usage, resource).incUses(1);
        Action action = findAction(resource, request.getMethod());
        actionUsage(usage, action).incUses(1);

        checkProtocol(action, requestUri, ramlUri);
        checkBaseUriParameters(hostMatch, pathMatch, action);
        checkQueryParameters(request.getQueryValues(), action);
        checkRequestHeaderParameters(request.getHeaderValues(), action);

        final Type type = findType(requestViolations, action, request, action.getBody(), "");
        if (type != null) {
            if (FormDecoder.supportsFormParameters(type.media)) {
                checkFormParameters(action, request.getFormValues(), type.mime);
            } else {
                checkSchema(requestViolations, action, request.getContent(), type, "");
            }
        }
        return action;
    }

    private void checkFormParameters(Action action, Values values, MimeType mimeType) {
        if (mimeType.getSchema() != null) {
            requestViolations.add("schema.superfluous", action, mimeType);
        }
        @SuppressWarnings("unchecked")
        final Map> formParameters = (Map) mimeType.getFormParameters();
        if (formParameters == null) {
            requestViolations.add("formParameters.missing", action, mimeType);
        } else {
            checkFormParametersValues(action, mimeType, values, formParameters);
        }
    }

    private void checkFormParametersValues(Action action, MimeType mimeType, Values values, Map> formParameters) {
        mimeTypeUsage(usage, action, mimeType).addFormParameters(
                new ParameterChecker(requestViolations)
                        .checkListParameters(formParameters, values, new Message("formParam", action))
        );
    }


    private void checkQueryParameters(Values values, Action action) {
        actionUsage(usage, action).addQueryParameters(
                new ParameterChecker(requestViolations)
                        .checkParameters(action.getQueryParameters(), values, new Message("queryParam", action))
        );
    }

    private void checkRequestHeaderParameters(Values values, Action action) {
        actionUsage(usage, action).addRequestHeaders(
                new ParameterChecker(requestViolations)
                        .acceptWildcard()
                        .ignoreX(ignoreXheaders)
                        .predefined(DefaultHeaders.REQUEST)
                        .checkParameters(action.getHeaders(), values, new Message("headerParam", action))
        );
    }

    private void checkBaseUriParameters(VariableMatcher hostMatch, VariableMatcher pathMatch, Action action) {
        final ParameterChecker paramChecker = new ParameterChecker(requestViolations).acceptUndefined();
        final Map> baseUriParams = getEffectiveBaseUriParams(action);
        paramChecker.checkListParameters(baseUriParams, hostMatch.getVariables(), new Message("baseUriParam", action));
        paramChecker.checkListParameters(baseUriParams, pathMatch.getVariables(), new Message("baseUriParam", action));
    }

    private Action findAction(Resource resource, String method) {
        Action action = resource.getAction(method);
        requestViolations.addAndThrowIf(action == null, "action.undefined", method, resource);
        return action;
    }

    private VariableMatcher getPathMatch(UriComponents requestUri, UriComponents ramlUri) {
        final VariableMatcher pathMatch = VariableMatcher.match(ramlUri.getPath(), requestUri.getPath());
        if (!pathMatch.isMatch()) {
            requestViolations.addAndThrow("baseUri.unmatched", requestUri.getUri(), raml.getBaseUri());
        }
        return pathMatch;
    }

    private VariableMatcher getHostMatch(UriComponents requestUri, UriComponents ramlUri) {
        final VariableMatcher hostMatch = VariableMatcher.match(ramlUri.getHost(), requestUri.getHost());
        if (!hostMatch.isCompleteMatch()) {
            requestViolations.addAndThrow("baseUri.unmatched", requestUri.getUri(), raml.getBaseUri());
        }
        return hostMatch;
    }

    private void checkProtocol(Action action, UriComponents requestUri, UriComponents ramlUri) {
        final List protocols = findProtocols(action, ramlUri.getScheme());
        requestViolations.addIf(!protocols.contains(protocolOf(requestUri.getScheme())), "protocol.undefined", requestUri.getScheme(), action);
    }

    private List findProtocols(Action action, String fallback) {
        List protocols = action.getProtocols();
        if (protocols == null || protocols.isEmpty()) {
            protocols = raml.getProtocols();
        }
        if (protocols == null || protocols.isEmpty()) {
            protocols = Collections.singletonList(Protocol.valueOf(fallback.toUpperCase()));
        }
        return protocols;
    }

    private Protocol protocolOf(String s) {
        if (s.equalsIgnoreCase("http")) {
            return Protocol.HTTP;
        }
        if (s.equalsIgnoreCase("https")) {
            return Protocol.HTTPS;
        }
        return null;
    }

    private Resource findResource(String resourcePath) {
        final Values values = new Values();
        final Resource resource = findResource(resourcePath, raml.getResources(), values);
        if (resource == null) {
            requestViolations.addAndThrow("resource.undefined", resourcePath);
        }
        checkUriParams(values, resource);
        return resource;
    }

    private void checkUriParams(Values values, Resource resource) {
        final ParameterChecker paramChecker = new ParameterChecker(requestViolations).acceptUndefined();
        for (Map.Entry> entry : values) {
            final AbstractParam uriParam = findUriParam(entry.getKey(), resource);
            if (uriParam != null) {
                paramChecker.checkParameter(uriParam, entry.getValue().get(0), new Message("uriParam", entry.getKey(), resource));
            }
        }
    }

    private AbstractParam findUriParam(String uriParam, Resource resource) {
        final UriParameter param = resource.getUriParameters().get(uriParam);
        if (param != null) {
            return param;
        }
        if (resource.getParentResource() != null) {
            return findUriParam(uriParam, resource.getParentResource());
        }
        return null;
    }

    private Map> getEffectiveBaseUriParams(Action action) {
        Map> params = new HashMap<>();
        if (action.getBaseUriParameters() != null) {
            params.putAll(action.getBaseUriParameters());
        }
        addNotSetBaseUriParams(action.getResource(), params);
        return params;
    }

    private void addNotSetBaseUriParams(Resource resource, Map> params) {
        if (resource.getBaseUriParameters() != null) {
            for (Map.Entry> entry : resource.getBaseUriParameters().entrySet()) {
                if (!params.containsKey(entry.getKey())) {
                    params.put(entry.getKey(), entry.getValue());
                }
            }
        }
        if (resource.getParentResource() != null) {
            addNotSetBaseUriParams(resource.getParentResource(), params);
        } else if (raml.getBaseUriParameters() != null) {
            for (Map.Entry entry : raml.getBaseUriParameters().entrySet()) {
                if (!params.containsKey(entry.getKey())) {
                    params.put(entry.getKey(), Collections.singletonList(entry.getValue()));
                }
            }
        }
    }

    private Resource findResource(String resourcePath, Map resources, Values values) {
        List matches = new ArrayList<>();
        for (Map.Entry entry : resources.entrySet()) {
            final VariableMatcher pathMatch = VariableMatcher.match(entry.getKey(), resourcePath);
            if (pathMatch.isCompleteMatch() || (pathMatch.isMatch() && pathMatch.getSuffix().startsWith("/"))) {
                matches.add(new ResourceMatch(pathMatch, entry.getValue()));
            }
        }
        Collections.sort(matches);
        for (ResourceMatch match : matches) {
            if (match.match.isCompleteMatch()) {
                values.addValues(match.match.getVariables());
                return match.resource;
            }
            if (match.match.isMatch()) {
                values.addValues(match.match.getVariables());
                return findResource(match.match.getSuffix(), match.resource.getResources(), values);
            }
        }
        return null;
    }

    private static class ResourceMatch implements Comparable {
        private final VariableMatcher match;
        private final Resource resource;

        private ResourceMatch(VariableMatcher match, Resource resource) {
            this.match = match;
            this.resource = resource;
        }

        @Override
        public int compareTo(ResourceMatch o) {
            return match.getVariables().size() - o.match.getVariables().size();
        }
    }

    public void checkResponse(Action action, RamlResponse response) {
        Response res = findResponse(action, response.getStatus());
        actionUsage(usage, action).addResponseCode("" + response.getStatus());
        checkResponseHeaderParameters(response.getHeaderValues(), action, "" + response.getStatus(), res);

        final String detail = new Message("response", response.getStatus()).toString();
        final Type type = findType(responseViolations, action, response, res.getBody(), detail);
        checkSchema(responseViolations, action, response.getContent(), type, detail);
    }

    private Type findType(RamlViolations violations, Action action, RamlMessage message, Map bodies, String detail) {
        if (isNoOrEmptyBodies(bodies)) {
            violations.addIf(hasContent(message), "body.superfluous", action, detail);
            return null;
        }

        if (message.getContentType() == null) {
            violations.addIf(hasContent(message) || !existSchemalessBody(bodies), "contentType.missing");
            return null;
        }
        MediaType targetType = MediaType.valueOf(message.getContentType());
        final MimeType mimeType = findMatchingMimeType(violations, action, bodies, targetType, detail);
        if (mimeType == null) {
            violations.add("mediaType.undefined", message.getContentType(), action, detail);
            return null;
        }
        return new Type(mimeType, targetType);
    }

    private static class Type {
        private final MimeType mime;
        private final MediaType media;

        private Type(MimeType mime, MediaType media) {
            this.mime = mime;
            this.media = media;
        }
    }

    private void checkSchema(RamlViolations violations, Action action, byte[] body, Type type, String detail) {
        if (type == null) {
            return;
        }
        final String schema = type.mime.getSchema();
        if (schema == null) {
            return;
        }
        final SchemaValidator validator = findSchemaValidator(type.media);
        if (validator == null) {
            violations.add("schemaValidator.missing", type.media, action, detail);
            return;
        }
        if (body.length == 0) {
            violations.add("body.empty", type.media, action, detail);
            return;
        }

        final String charset = type.media.getCharset("iso-8859-1");
        try {
            final String content = new String(body, charset);
            final String refSchema = raml.getConsolidatedSchemas().get(schema);
            final String schemaToUse = refSchema != null ? refSchema : schema;
            validator.validate(content, schemaToUse, violations, new Message("schema.mismatch", action, detail, type.mime, content));
        } catch (UnsupportedEncodingException e) {
            violations.add("charset.invalid", charset);
        }
    }

    private void checkResponseHeaderParameters(Values values, Action action, String responseCode, Response response) {
        responseUsage(usage, action, responseCode).addResponseHeaders(
                new ParameterChecker(responseViolations)
                        .acceptWildcard()
                        .ignoreX(ignoreXheaders)
                        .predefined(DefaultHeaders.RESPONSE)
                        .checkParameters(response.getHeaders(), values, new Message("headerParam", action))
        );
    }

    private Response findResponse(Action action, int status) {
        Response res = action.getResponses().get("" + status);
        responseViolations.addAndThrowIf(res == null, "responseCode.undefined", status, action);
        return res;
    }

    private SchemaValidator findSchemaValidator(MediaType mediaType) {
        for (SchemaValidator validator : schemaValidators) {
            if (validator.supports(mediaType)) {
                return validator;
            }
        }
        return null;
    }

    private boolean isNoOrEmptyBodies(Map bodies) {
        return bodies == null || bodies.isEmpty() || (bodies.size() == 1 && bodies.containsKey(null));
    }

    private boolean hasContent(RamlMessage message) {
        return message.getContent() != null && message.getContent().length > 0;
    }

    private boolean existSchemalessBody(Map bodies) {
        for (MimeType mimeType : bodies.values()) {
            if (mimeType.getSchema() == null) {
                return true;
            }
        }
        return false;
    }

    private MimeType findMatchingMimeType(RamlViolations violations, Action action, Map bodies, MediaType targetType, String detail) {
        MimeType res = null;
        try {
            for (Map.Entry entry : bodies.entrySet()) {
                if (targetType.isCompatibleWith(MediaType.valueOf(entry.getKey()))) {
                    if (res == null) {
                        res = entry.getValue();
                    } else {
                        violations.add("mediaType.ambiguous", res, entry.getValue(), action, detail);
                    }
                }
            }
        } catch (InvalidMediaTypeException e) {
            violations.add("mediaType.illegal", e.getMimeType());
        }
        return res;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy