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

com.google.api.server.spi.config.model.ApiMethodConfig Maven / Gradle / Ivy

There is a newer version: 2.2.2
Show newest version
/*
 * 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.model;

import com.google.api.server.spi.Constant;
import com.google.api.server.spi.EndpointMethod;
import com.google.api.server.spi.TypeLoader;
import com.google.api.server.spi.config.AuthLevel;
import com.google.api.server.spi.config.Authenticator;
import com.google.api.server.spi.config.PeerAuthenticator;
import com.google.api.server.spi.config.model.ApiParameterConfig.Classification;
import com.google.api.server.spi.config.scope.AuthScopeExpression;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Flattened method configuration for a swarm endpoint method.  Data generally originates from
 * {@link com.google.api.server.spi.config.ApiMethod} annotations.
 *
 * @author Eric Orth
 */
public class ApiMethodConfig {

  private enum RestMethod {
    LIST("list", "GET") {
      @Override
      public String guessResourceName(
          ApiConfig config, EndpointMethod method, Map> classTypes) {
        TypeToken returnType = method.getReturnType();
        if (isValidCollectionType(returnType)) {
          return Types.getSimpleName(
              Types.getTypeParameter(returnType, 0), config.getSerializationConfig()).toLowerCase();
        }
        return null;
      }

      private boolean isValidCollectionType(TypeToken type) {
        return type.isSubtypeOf(Collection.class) || Types.isCollectionResponseType(type);
      }
    },
    GET("get", "GET"),
    INSERT("insert", "POST"),
    UPDATE("update", "PUT"),
    DELETE("delete", "DELETE") {
      @Override
      public String guessResourceName(
          ApiConfig config, EndpointMethod method, Map> classTypes) {
        String methodName = method.getMethod().getName();
        return methodNamePrefix.length() >= methodName.length() ? null :
            methodName.substring(methodNamePrefix.length()).toLowerCase();
      }
    },
    REMOVE("remove", "DELETE") {
      @Override
      public String guessResourceName(
          ApiConfig config, EndpointMethod method, Map> classTypes) {
        String methodName = method.getMethod().getName();
        return methodNamePrefix.length() >= methodName.length() ? null :
            methodName.substring(methodNamePrefix.length()).toLowerCase();
      }
    },
    DEFAULT("", "POST") {
      @Override
      public String guessResourceName(
          ApiConfig config, EndpointMethod method, Map> classTypes) {
        return null;
      }
    };

    protected final String methodNamePrefix;
    private final String httpMethod;

    /**
     * Specifies a default REST method prefix, as well as what HTTP method it should use by default.
     *
     * @param methodNamePrefix A method name prefix.
     * @param httpMethod The default HTTP method for this prefix.
     */
    RestMethod(String methodNamePrefix, String httpMethod) {
      this.methodNamePrefix = methodNamePrefix;
      this.httpMethod = httpMethod;
    }

    /**
     * Gets the method name prefix for this instance.
     *
     * @return The method name prefix.
     */
    public String getMethodNamePrefix() {
      return this.methodNamePrefix;
    }

    /**
     * Gets the default HTTP method for this instance.
     *
     * @return The HTTP method.
     */
    public String getHttpMethod() {
      return this.httpMethod;
    }

    /**
     * Guesses a resource name based off a prefix.
     *
     * @return The HTTP method.
     */
    public String guessResourceName(
        ApiConfig config, EndpointMethod method, Map> classTypes) {
      return Types.getSimpleName(method.getReturnType(), config.getSerializationConfig())
          .toLowerCase();
    }
  }

  private final String endpointMethodName;

  private final List parameterConfigs;
  private final ApiClassConfig apiClassConfig;

  private String name;
  private String description;
  private String path;
  private String canonicalPath;
  private String httpMethod;

  // If null, get a default from apiConfig.
  // TODO: When splitting out a builder class, pull in actual default on build().
  private AuthLevel authLevel;
  private AuthScopeExpression scopeExpression;
  private List audiences;
  private ApiIssuerAudienceConfig issuerAudiences;
  private List clientIds;
  private List> authenticators;
  private List> peerAuthenticators;
  private boolean ignored = false;
  private Boolean apiKeyRequired;
  private TypeToken returnType;
  private List metricCosts;

  private final TypeLoader typeLoader;

  public ApiMethodConfig(EndpointMethod method, TypeLoader typeLoader,
      ApiClassConfig apiClassConfig) {
    this.endpointMethodName = method.getMethod().getName();
    this.parameterConfigs = new ArrayList<>();
    this.apiClassConfig = apiClassConfig;
    this.typeLoader = typeLoader;
    setDefaults(method, typeLoader, apiClassConfig.getResource());
  }

  public ApiMethodConfig(ApiMethodConfig original, ApiClassConfig apiClassConfig) {
    this.endpointMethodName = original.endpointMethodName;
    this.apiClassConfig = apiClassConfig;
    this.name = original.name;
    this.path = original.path;
    this.canonicalPath = original.canonicalPath;
    this.description = original.description;
    this.httpMethod = original.httpMethod;
    this.scopeExpression = original.scopeExpression;
    this.audiences = original.audiences == null ? null : new ArrayList<>(original.audiences);
    this.issuerAudiences = original.issuerAudiences;
    this.clientIds = original.clientIds == null ? null : new ArrayList<>(original.clientIds);
    this.authenticators =
        original.authenticators == null ? null : new ArrayList<>(original.authenticators);
    this.peerAuthenticators =
        original.peerAuthenticators == null ? null : new ArrayList<>(original.peerAuthenticators);
    this.ignored = original.ignored;
    this.apiKeyRequired = original.apiKeyRequired;
    this.returnType = original.returnType;
    this.typeLoader = original.typeLoader;
    this.metricCosts = original.metricCosts;

    // Parameter configs are mutable, so we need to do a deep copy.
    this.parameterConfigs = new ArrayList<>(original.parameterConfigs.size());
    for (ApiParameterConfig parameter : original.parameterConfigs) {
      parameterConfigs.add(new ApiParameterConfig(parameter, this));
    }
  }

  /**
   * Sets all fields to their default value to be used if not set otherwise.  Override to change the
   * default configuration.
   */
  protected void setDefaults(EndpointMethod endpointMethod, TypeLoader typeLoader,
      String apiDefaultResource) {
    Method method = endpointMethod.getMethod();
    RestMethod restMethod = getRestMethod(method);
    String resourceTypeName;
    if (apiDefaultResource != null) {
      resourceTypeName = apiDefaultResource.toLowerCase();
    } else {
      resourceTypeName = restMethod.guessResourceName(
          apiClassConfig.getApiConfig(), endpointMethod, typeLoader.getClassTypes());
    }

    name = null;
    httpMethod = Preconditions.checkNotNull(restMethod.getHttpMethod(), "httpMethod");
    setPath(Preconditions.checkNotNull(
        resourceTypeName == null ? method.getName() : resourceTypeName.toLowerCase(), "path"));
    authLevel = AuthLevel.UNSPECIFIED;
    scopeExpression = null;
    audiences = null;
    issuerAudiences = ApiIssuerAudienceConfig.UNSPECIFIED;
    clientIds = null;
    authenticators = null;
    peerAuthenticators = null;
    ignored = false;
    apiKeyRequired = null;
    returnType = endpointMethod.getReturnType();
    metricCosts = ImmutableList.of();
  }

  private RestMethod getRestMethod(Method method) {
    String methodName = method.getName();
    for (RestMethod entry : RestMethod.values()) {
      if (methodName.startsWith(entry.getMethodNamePrefix())) {
        return entry;
      }
    }
    throw new AssertionError("It's impossible for method" + method + " to map to no REST path.");
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    } else if (o instanceof ApiMethodConfig) {
      ApiMethodConfig config = (ApiMethodConfig) o;
      return Objects.equals(endpointMethodName, config.endpointMethodName) &&
          parameterConfigs.equals(config.parameterConfigs) && Objects.equals(name, config.name) &&
          Objects.equals(path, config.path) && Objects.equals(httpMethod, config.httpMethod) &&
          Objects.equals(scopeExpression, config.scopeExpression) &&
          Objects.equals(audiences, config.audiences) &&
          Objects.equals(issuerAudiences, config.issuerAudiences) &&
          Objects.equals(clientIds, config.clientIds) &&
          Objects.equals(authenticators, config.authenticators) &&
          Objects.equals(peerAuthenticators, config.peerAuthenticators) &&
          Objects.equals(typeLoader, config.typeLoader) &&
          ignored == config.ignored &&
          apiKeyRequired == config.apiKeyRequired &&
          Objects.equals(returnType, config.returnType) &&
          Objects.equals(metricCosts, config.metricCosts);
    } else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    return Objects.hash(endpointMethodName, parameterConfigs, name, path, httpMethod,
        scopeExpression, audiences, clientIds, authenticators, peerAuthenticators, typeLoader,
        ignored, issuerAudiences, apiKeyRequired, returnType, metricCosts);
  }

  public ApiClassConfig getApiClassConfig() {
    return apiClassConfig;
  }

  /**
   * Shorthand for {@code getApiClassConfig().getApiConfig()}.
   */
  public ApiConfig getApiConfig() {
    return apiClassConfig.getApiConfig();
  }

  public String getEndpointMethodName() {
    return endpointMethodName;
  }

  /**
   * Generates, using class name and java method name, a dot-separated full name for this endpoint
   * method.  This is different from the method name from {@code getFullMethodName}, which is used
   * to uniquely identify the method within the context of the API. The java method name, however,
   * is used to identify and reflectively call the actual java method.
   */
  public String getFullJavaName() {
    return apiClassConfig.getApiClassJavaName() + "." + getEndpointMethodName();
  }

  /**
   * Adds the given parameter to the configuration and updates the path to add the new parameter if
   * it is non-optional and has no default.
   */
  public ApiParameterConfig addParameter(String name, String description, boolean nullable,
      String defaultValue, TypeToken type) {
    ApiParameterConfig config =
        new ApiParameterConfig(this, name, description, nullable, defaultValue, type, typeLoader);
    parameterConfigs.add(config);

    if (config.getClassification() != Classification.INJECTED && name != null && !nullable
        && defaultValue == null) {
      String pathFragment = "{" + name + "}";
      if (!path.contains(pathFragment)) {
        setPath(path + "/" + pathFragment);
      }
    }

    return config;
  }

  public List getParameterConfigs() {
    return parameterConfigs;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getName() {
    if (name != null) {
      return name;
    } else if (apiClassConfig.getResource() != null) {
      return String.format("%s.%s", apiClassConfig.getResource(), endpointMethodName);
    } else {
      if (apiClassConfig.getApiClassJavaSimpleName().isEmpty()) {
        return String.format("%s", endpointMethodName);
      } else {
        return String.format(
            "%s.%s", apiClassConfig.getApiClassJavaSimpleName(), endpointMethodName);
      }
    }
  }

  /*
   * Parts in dot delimited external method names such as JSON-RPC method names
   * must be camel-cased alpha-numeric.
   * [a-z][a-zA-Z0-9]*
   */
  public static String methodNameFormatter(String methodName) {
    StringBuilder builder = new StringBuilder();
    for (String s : methodName.split("\\.")) {
      if (!s.isEmpty()) {
        builder.append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).append('.');
      }
    }
    if (builder.length() == 0) {
      return builder.toString();
    } else {
      return builder.deleteCharAt(builder.length() - 1).toString();
    }
  }

  /**
   * Generates, using API name and method name, a dot-separated full name for this endpoint method.
   * The name is sanitized by {@code methodNameFormatter}.
   */
  public String getFullMethodName() {
    return methodNameFormatter(apiClassConfig.getApiConfig().getName() + "." + getName());
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public void setPath(String path) {
    this.path = path;
    if (path.startsWith("/")) {
      canonicalPath = path.substring(1);
    } else {
      canonicalPath = getApiConfig().getName() + "/" + getApiConfig().getVersion() + "/" + path;
    }
  }

  public String getPath() {
    return path;
  }

  public String getCanonicalPath() {
    return canonicalPath;
  }

  public void setHttpMethod(String httpMethod) {
    this.httpMethod = httpMethod;
  }

  public String getHttpMethod() {
    return httpMethod;
  }

  /**
   * Generates a string representing the signature of the method when called using REST.  Can be
   * used to determine ambiguity of such REST calls.
   *
   * For example, a method with path "getFoo/{idA}/bar/{idB}" using http method GET would generate
   * the rest path "GET getFoo/{}/bar/{}".
   */
  public String getRestfulSignature() {
    return getHttpMethod() + " " + getPath().replaceAll("\\{([^\\}]*)\\}", "\\{\\}");
  }

  public void setAuthLevel(AuthLevel authLevel) {
    this.authLevel = authLevel;
  }

  public AuthLevel getAuthLevel() {
    return authLevel != AuthLevel.UNSPECIFIED ? authLevel : apiClassConfig.getAuthLevel();
  }

  public void setScopeExpression(AuthScopeExpression scopeExpression) {
    this.scopeExpression = scopeExpression;
  }

  public AuthScopeExpression getScopeExpression() {
    return scopeExpression != null ? scopeExpression : apiClassConfig.getScopeExpression();
  }

  public void setAudiences(List audiences) {
    this.audiences = audiences;
  }

  public List getAudiences() {
    return audiences != null ? audiences : apiClassConfig.getAudiences();
  }

  public void setIssuerAudiences(ApiIssuerAudienceConfig issuerAudiences) {
    Preconditions.checkNotNull(issuerAudiences, "issuerAudiences should never be null");
    this.issuerAudiences = issuerAudiences;
    if (issuerAudiences.hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME)) {
      getApiClassConfig().getApiConfig().ensureGoogleIssuer();
    }
  }

  public ApiIssuerAudienceConfig getIssuerAudiences() {
    return issuerAudiences.isSpecified() ? issuerAudiences : apiClassConfig.getIssuerAudiences();
  }

  public void setClientIds(List clientIds) {
    this.clientIds = clientIds;
  }

  public List getClientIds() {
    return clientIds != null ? clientIds : apiClassConfig.getClientIds();
  }

  public void setAuthenticators(List> authenticators) {
    this.authenticators = authenticators;
  }

  public List> getAuthenticators() {
    return authenticators != null ? authenticators : apiClassConfig.getAuthenticators();
  }

  public void setPeerAuthenticators(List> peerAuthenticators) {
    this.peerAuthenticators = peerAuthenticators;
  }

  public List> getPeerAuthenticators() {
    return peerAuthenticators != null ? peerAuthenticators : apiClassConfig.getPeerAuthenticators();
  }

  public void setIgnored(boolean ignored) {
    this.ignored = ignored;
  }

  public boolean isIgnored() {
    return ignored;
  }

  public void setApiKeyRequired(boolean apiKeyRequired) {
    this.apiKeyRequired = apiKeyRequired;
  }

  public boolean isApiKeyRequired() {
    return apiKeyRequired != null ? apiKeyRequired : apiClassConfig.isApiKeyRequired();
  }

  /**
   * Gets parameters in current path.
   */
  public Collection getPathParameters() {
    Pattern pathPattern = java.util.regex.Pattern.compile("\\{([^\\}]*)\\}");
    Matcher pathMatcher = pathPattern.matcher(path);

    Collection pathParameters = new LinkedHashSet<>();
    while (pathMatcher.find()) {
      pathParameters.add(pathMatcher.group(1));
    }

    return pathParameters;
  }

  public TypeToken getReturnType() {
    return returnType;
  }

  /**
   * Returns whether or not the method has a resource (is non-void) in the response.
   */
  public boolean hasResourceInResponse() {
    Type returnType = getReturnType().getRawType();
    return returnType != Void.TYPE && returnType != Void.class;
  }

  public void setMetricCosts(List metricCosts) {
    this.metricCosts = metricCosts;
  }

  public List getMetricCosts() {
    return metricCosts;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy