com.google.api.server.spi.config.validation.ApiConfigValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.api.server.spi.config.validation.ApiConfigValidator.java

Source

/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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 com.google.api.server.spi.config.validation;

import com.google.api.client.util.Strings;
import com.google.api.server.spi.config.ApiConfigInconsistency;
import com.google.api.server.spi.config.Transformer;
import com.google.api.server.spi.config.model.ApiClassConfig;
import com.google.api.server.spi.config.model.ApiConfig;
import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig;
import com.google.api.server.spi.config.model.ApiIssuerConfigs;
import com.google.api.server.spi.config.model.ApiIssuerConfigs.IssuerConfig;
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.config.model.ApiNamespaceConfig;
import com.google.api.server.spi.config.model.ApiParameterConfig;
import com.google.api.server.spi.config.model.Serializers;
import com.google.api.server.spi.config.model.Types;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Validation provider for Swarm endpoint configurations.  Contains all SPI-level checks for what
 * constitutes a valid API or configuration.
 *
 * @author Eric Orth
 */
public class ApiConfigValidator {
    private static final Pattern API_NAME_PATTERN = Pattern.compile("^[a-z]+[A-Za-z0-9]*$");

    private static final Pattern API_METHOD_NAME_PATTERN = Pattern.compile("^\\w+(\\.\\w+)*$");

    /**
     * Validates all configurations for a single API.  Makes sure the API-level configuration matches
     * for all classes and that the contained configuration is valid and can be turned into a *.api
     * file.  Only checks for swarm-specific validity.  Apiary FE may still dislike a config for its
     * own reasons.
     *
     * @throws ApiConfigInvalidException on any invalid API-wide configuration.
     * @throws ApiClassConfigInvalidException on any invalid API class configuration.
     * @throws ApiMethodConfigInvalidException on any invalid API method configuration.
     * @throws ApiParameterConfigInvalidException on any invalid API parameter configuration.
     */
    public void validate(Iterable<? extends ApiConfig> apiConfigs) throws ApiConfigInvalidException,
            ApiClassConfigInvalidException, ApiMethodConfigInvalidException, ApiParameterConfigInvalidException {
        if (Iterables.isEmpty(apiConfigs)) {
            return;
        }

        Map<String, ApiMethodConfig> restfulSignatures = Maps.newHashMap();

        Iterator<? extends ApiConfig> i = apiConfigs.iterator();
        ApiConfig first = i.next();
        validate(first, restfulSignatures);

        while (i.hasNext()) {
            ApiConfig config = i.next();
            Iterable<ApiConfigInconsistency<Object>> inconsistencies = config
                    .getConfigurationInconsistencies(first);
            if (!Iterables.isEmpty(inconsistencies)) {
                throw new InconsistentApiConfigurationException(config, first, inconsistencies);
            }
            validate(config, restfulSignatures);
        }
    }

    /**
     * Makes sure the contained configuration is valid and can be turned into a *.api file.  Only
     * checks for swarm-specific validity.  Apiary FE may still dislike a config for its own reasons.
     *
     * @throws ApiClassConfigInvalidException on any invalid API class or API configuration.
     * @throws ApiMethodConfigInvalidException on any invalid API method configuration.
     * @throws ApiParameterConfigInvalidException on any invalid API parameter configuration.
     */
    public void validate(ApiConfig config) throws ApiClassConfigInvalidException, ApiMethodConfigInvalidException,
            ApiParameterConfigInvalidException, ApiConfigInvalidException {
        validate(config, new HashMap<String, ApiMethodConfig>());
    }

    private void validate(ApiConfig config, Map<String, ApiMethodConfig> restfulSignatures)
            throws ApiClassConfigInvalidException, ApiMethodConfigInvalidException,
            ApiParameterConfigInvalidException, ApiConfigInvalidException {
        validateApiConfig(config);
        validateThirdPartyAuth(config.getApiClassConfig());
        validateMethods(config.getApiClassConfig().getMethods(), restfulSignatures);
    }

    private void validateApiConfig(ApiConfig config)
            throws InvalidNamespaceException, InvalidApiNameException, InvalidIssuerValueException.ForApi {
        validateApiName(config);
        validateNamespaceConfig(config.getNamespaceConfig(), config.getApiClassConfig());
        validateThirdPartyAuth(config);
    }

    private void validateApiName(ApiConfig config) throws InvalidApiNameException {
        if (!API_NAME_PATTERN.matcher(config.getName()).matches()) {
            throw new InvalidApiNameException(config, config.getName());
        }
    }

    private void validateNamespaceConfig(ApiNamespaceConfig config, ApiClassConfig apiConfig)
            throws InvalidNamespaceException {
        boolean allUnspecified = config.getOwnerDomain().isEmpty() && config.getOwnerName().isEmpty()
                && config.getPackagePath().isEmpty();
        boolean ownerFullySpecified = !config.getOwnerDomain().isEmpty() && !config.getOwnerName().isEmpty();

        // Either everything must be fully unspecified or owner domain/name must both be specified.
        if (!allUnspecified && !ownerFullySpecified) {
            throw new InvalidNamespaceException(apiConfig);
        }
    }

    private void validateThirdPartyAuth(ApiConfig config) throws InvalidIssuerValueException.ForApi {
        String error = checkIssuers(config.getIssuers());
        if (error != null) {
            throw new InvalidIssuerValueException.ForApi(config, error);
        }
        error = checkIssuerAudiences(config.getIssuers(), config.getIssuerAudiences());
        if (error != null) {
            throw new InvalidIssuerValueException.ForApi(config, error);
        }
    }

    private void validateThirdPartyAuth(ApiClassConfig config) throws InvalidIssuerValueException.ForApiClass {
        String error = checkIssuerAudiences(config.getApiConfig().getIssuers(), config.getIssuerAudiences());
        if (error != null) {
            throw new InvalidIssuerValueException.ForApiClass(config, error);
        }
    }

    private void validateMethods(ApiClassConfig.MethodConfigMap configMap,
            Map<String, ApiMethodConfig> restfulSignatures) throws ApiClassConfigInvalidException,
            ApiMethodConfigInvalidException, ApiParameterConfigInvalidException {
        Map<String, ApiMethodConfig> javaMethodNames = Maps.newHashMap();
        for (ApiMethodConfig methodConfig : configMap.values()) {
            if (!methodConfig.isIgnored()) {
                validateRestSignatureUnique(methodConfig, restfulSignatures);
                validateBackendMethodNameUnique(methodConfig, javaMethodNames);
                validateMethod(methodConfig);
            }
        }
    }

    private void validateRestSignatureUnique(ApiMethodConfig methodConfig,
            Map<String, ApiMethodConfig> restfulSignatures) throws DuplicateRestPathException {
        String restSignature = methodConfig.getRestfulSignature();
        ApiMethodConfig seenMethod = restfulSignatures.get(restSignature);
        if (seenMethod == null) {
            restfulSignatures.put(restSignature, methodConfig);
        } else {
            throw new DuplicateRestPathException(methodConfig.getApiClassConfig(), restSignature,
                    methodConfig.getName(), seenMethod.getName());
        }
    }

    private void validateBackendMethodNameUnique(ApiMethodConfig methodConfig,
            Map<String, ApiMethodConfig> javaMethodNames) throws OverloadedMethodException {
        String javaName = methodConfig.getFullJavaName();
        ApiMethodConfig seenMethod = javaMethodNames.get(javaName);
        if (seenMethod == null) {
            javaMethodNames.put(javaName, methodConfig);
        } else {
            throw new OverloadedMethodException(methodConfig.getApiClassConfig(), javaName, methodConfig.getName(),
                    seenMethod.getName());
        }
    }

    private void validateMethod(ApiMethodConfig config)
            throws ApiMethodConfigInvalidException, ApiParameterConfigInvalidException {
        if (!API_METHOD_NAME_PATTERN.matcher(config.getName()).matches()) {
            throw new InvalidMethodNameException(config, config.getName());
        }

        validateNullaryConstructor(config.getAuthenticators(), config, "custom authenticator");
        validateNullaryConstructor(config.getPeerAuthenticators(), config, "custom peer authenticator");

        Set<String> parameterNames = Sets.newHashSet();
        for (ApiParameterConfig parameter : config.getParameterConfigs()) {
            validateParameter(parameter, parameterNames, config.getPathParameters());
        }
        validateThirdPartyAuth(config);
    }

    private void validateThirdPartyAuth(ApiMethodConfig config) throws InvalidIssuerValueException.ForApiMethod {
        String error = checkIssuerAudiences(config.getApiClassConfig().getApiConfig().getIssuers(),
                config.getIssuerAudiences());
        if (error != null) {
            throw new InvalidIssuerValueException.ForApiMethod(config, error);
        }
    }

    private void validateNullaryConstructor(List<?> classes, ApiMethodConfig config, String description)
            throws ApiMethodConfigInvalidException {
        if (classes == null) {
            return;
        }
        for (Object clazz : classes) {
            assert clazz instanceof Class<?>;
            boolean nullaryFound = false;
            for (Constructor<?> constructor : ((Class<?>) clazz).getConstructors()) {
                if (isConstructorPublicNullary(constructor)) {
                    nullaryFound = true;
                    break;
                }
            }
            if (!nullaryFound) {
                throw new InvalidConstructorException(((Class<?>) clazz), config, description);
            }
        }
    }

    private static boolean isConstructorPublicNullary(Constructor<?> constructor) {
        return constructor.getParameterTypes().length == 0 && (constructor.getModifiers() & Modifier.PUBLIC) != 0;
    }

    private void validateParameter(ApiParameterConfig parameter, Set<String> parameterNames,
            Collection<String> pathParameters) throws ApiParameterConfigInvalidException {
        try {
            validateParameterSerializers(parameter, parameter.getSerializers(), parameter.getType());
            validateParameterSerializers(parameter, parameter.getRepeatedItemSerializers(),
                    parameter.getRepeatedItemType());
        } catch (IllegalStateException e) {
            // TODO: Switch to something less fragile for this error type.  I'm not comfortable
            // trusting that any ISE coming out of the serializer code will always be the multiple
            // serializer error that it is right now.  And it shouldn't be up to any code outside of the
            // validator to decide the error message.  Ideal would be that the error originates
            // here, so maybe the getSerializer chain should be changed to getSerilizer*s* with an
            // exception thrown here for multiple results.
            throw new ApiParameterConfigInvalidException(parameter, e.getMessage());
        }

        TypeToken<?> type;
        if (parameter.isRepeated()) {
            type = parameter.getRepeatedItemSerializedType();
            if (Types.isArrayType(type)) {
                throw new NestedCollectionException(parameter, type);
            }
        } else {
            type = parameter.getSchemaBaseType();
        }

        switch (parameter.getClassification()) {
        case INJECTED:
            // No classification-specific validation for injected parameters.
            break;
        case API_PARAMETER:
            validateApiParameter(parameter, parameterNames, pathParameters, type);
            break;
        case RESOURCE:
            validateResourceParameter(parameter, type);
            break;
        case UNKNOWN:
            // Unknown types are never allowed.
            throw new GenericTypeException(parameter);
        default:
            throw new AssertionError("Unrecognized parameter classification: " + parameter.getClassification());
        }
    }

    private void validateParameterSerializers(ApiParameterConfig config,
            List<Class<? extends Transformer<?, ?>>> serializers, TypeToken<?> parameterType)
            throws ApiParameterConfigInvalidException {
        if (serializers.isEmpty()) {
            return;
        }

        if (serializers.size() > 1) {
            throw new MultipleTransformersException(config, serializers);
        }

        TypeToken<?> sourceType = Serializers.getSourceType(serializers.get(0));
        TypeToken<?> serializedType = Serializers.getTargetType(serializers.get(0));

        if (sourceType == null || serializedType == null) {
            throw new NoTransformerInterfaceException(config, serializers.get(0));
        }

        if (!sourceType.isSupertypeOf(parameterType)) {
            throw new WrongTransformerTypeException(config, serializers.get(0), parameterType, sourceType);
        }
    }

    private void validateApiParameter(ApiParameterConfig parameter, Set<String> parameterNames,
            Collection<String> pathParameters, TypeToken<?> type) throws ApiParameterConfigInvalidException {
        if (parameter.getName() == null) {
            throw new MissingParameterNameException(parameter, type);
        }
        if (!parameterNames.add(parameter.getName())) {
            throw new DuplicateParameterNameException(parameter);
        }
        if ((parameter.getNullable() || parameter.getDefaultValue() != null)
                && pathParameters.contains(parameter.getName())) {
            throw new InvalidParameterAnnotationsException(parameter);
        }
    }

    private void validateResourceParameter(ApiParameterConfig parameter, TypeToken<?> type)
            throws ApiParameterConfigInvalidException {
        if (parameter.isRepeated()) {
            throw new CollectionResourceException(parameter, parameter.getRepeatedItemSerializedType(),
                    parameter.getSchemaBaseType());
        }
        if (parameter.getName() != null) {
            throw new NamedResourceException(parameter, type);
        }
    }

    private static String checkIssuers(ApiIssuerConfigs issuers) {
        if (!issuers.isSpecified()) {
            return null;
        }
        for (IssuerConfig issuer : issuers.asMap().values()) {
            if (Strings.isNullOrEmpty(issuer.getName())) {
                return "issuer name cannot be blank";
            } else if (Strings.isNullOrEmpty(issuer.getIssuer())) {
                return "issuer '" + issuer.getName() + "' cannot have a blank issuer value";
            }
        }
        return null;
    }

    private static String checkIssuerAudiences(ApiIssuerConfigs issuerConfigs,
            ApiIssuerAudienceConfig issuerAudiences) {
        if (!issuerAudiences.isEmpty()) {
            return null;
        }
        for (Map.Entry<String, Collection<String>> entry : issuerAudiences.asMap().entrySet()) {
            if (!issuerConfigs.hasIssuer(entry.getKey())) {
                return "cannot specify audiences for unknown issuer '" + entry.getKey() + "'";
            }
            for (String audience : entry.getValue()) {
                if (Strings.isNullOrEmpty(audience)) {
                    return "issuer '" + entry.getKey() + "' cannot have null or blank audiences";
                }
            }
        }
        return null;
    }
}