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

com.google.api.tools.framework.aspects.http.RestAnalyzer Maven / Gradle / Ivy

/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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.tools.framework.aspects.http;

import com.google.api.tools.framework.aspects.documentation.model.ResourceAttribute;
import com.google.api.tools.framework.aspects.http.RestPatterns.MethodPattern;
import com.google.api.tools.framework.aspects.http.model.CollectionAttribute;
import com.google.api.tools.framework.aspects.http.model.CollectionName;
import com.google.api.tools.framework.aspects.http.model.HttpAttribute;
import com.google.api.tools.framework.aspects.http.model.HttpAttribute.LiteralSegment;
import com.google.api.tools.framework.aspects.http.model.HttpAttribute.PathSegment;
import com.google.api.tools.framework.aspects.http.model.MethodKind;
import com.google.api.tools.framework.aspects.http.model.RestKind;
import com.google.api.tools.framework.aspects.http.model.RestMethod;
import com.google.api.tools.framework.aspects.versioning.model.ApiVersionUtil;
import com.google.api.tools.framework.model.MessageType;
import com.google.api.tools.framework.model.Method;
import com.google.api.tools.framework.model.Model;
import com.google.api.tools.framework.model.TypeRef;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.BiMap;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Rest analyzer. Determines {@link RestMethod} associated with method and http binding. Also
 * computes collections and estimates their resource types.
 *
 * 

Rest conformance violating warnings are reported in the lint phase, this module will never * produce errors or warnings and always returns well-defined output for each method. */ public class RestAnalyzer { /** * If experiment is set, the RestAnalyzer will shorten collection names using a heuristic. */ private static final String SHORTEN_COLLECTION_NAMES = "shorten-collection-names"; private final HttpConfigAspect aspect; private final Map intermediateCollectionMap = new TreeMap<>(); private final Map collectionMap = new TreeMap<>(); /** * Creates a rest analyzer which reports errors via the given aspect. */ RestAnalyzer(HttpConfigAspect aspect) { this.aspect = aspect; } /** * Finalizes rest analysis, delivering the collections used. */ List finalizeAndGetCollections() { // Compute the resource types for each collection. We need to have all collections fully // built before this can be done. // // In the first pass, we walk over all messages and collect information from the // resource attribute as derived from a doc instruction. In the second pass, for those // collections which still have no resource, we run a heuristic to identify the resource. Map definedResources = Maps.newLinkedHashMap(); for (TypeRef type : aspect.getModel().getSymbolTable().getDeclaredTypes()) { if (!type.isMessage()) { continue; } MessageType message = type.getMessageType(); List definitions = message.getAttribute(ResourceAttribute.KEY); if (definitions != null) { for (ResourceAttribute definition : definitions) { definedResources.put(definition.collection(), type); } } } if (aspect.getModel().getExperiments().isExperimentEnabled(SHORTEN_COLLECTION_NAMES)) { assignShortNames(intermediateCollectionMap.values()); } for (CollectionAttribute collection : intermediateCollectionMap.values()) { // note: nested loop needed to reach duplicate REST methods for (String restMethodName : collection.getRestMethodNames()) { for (RestMethod restMethod : collection.getRestMethodWithDuplicates(restMethodName)) { addMethodToCollection( collectionMap, restMethod, restMethod.getBaseRestCollectionName()); } } } ImmutableList.Builder result = ImmutableList.builder(); for (CollectionAttribute collection : collectionMap.values()) { TypeRef type = definedResources.get(collection.getFullName()); if (type == null) { // No defined resource association, run heuristics. type = new ResourceTypeSelector(aspect.getModel(), collection.getMethods()).getCandiateResourceType(); } collection.setResourceType(type); result.add(collection); } List collections = result.build(); return collections; } /** * Analyzes the given method and http config and returns a rest method. */ RestMethod analyzeMethod(Method method, HttpAttribute httpConfig) { // First check whether this is a special method. RestMethod restMethod = createSpecialMethod(method, httpConfig); String configuredCollectionName = ""; if (restMethod == null) { // Search for the first matching method pattern. MethodMatcher matcher = null; for (MethodPattern pattern : RestPatterns.METHOD_PATTERNS) { matcher = new MethodMatcher(pattern, method, httpConfig); if (matcher.matches()) { break; } matcher = null; } if (matcher != null) { restMethod = matcher.createRestMethod(configuredCollectionName); } else { restMethod = createCustomMethod(method, httpConfig, ""); restMethod.setHasValidRestPattern(false); } } // Add method to collections and name-to-collection maps. // If name is manually configured from the HttpRule, use the final collection map so // that the configured name will not be shortened if the shorten-collection-names experiment is // enabled. Otherwise, use an intermediate map, which is potentially subject to name shortening. if (Strings.isNullOrEmpty(configuredCollectionName)) { addMethodToCollection( intermediateCollectionMap, restMethod, restMethod.getBaseRestCollectionName()); } else { addMethodToCollection(collectionMap, restMethod, configuredCollectionName); } return restMethod; } private void addMethodToCollection(Map collectionMap, RestMethod restMethod, String baseCollectionName) { String version = restMethod.getVersion(); String versionedCollectionName = version + "." + baseCollectionName; CollectionAttribute collection = collectionMap.get(versionedCollectionName); if (collection == null) { collection = new CollectionAttribute(aspect.getModel(), baseCollectionName, version); collectionMap.put(versionedCollectionName, collection); } collection.addMethod(restMethod); } // Determines whether to create a special rest method. Returns null if no special rest method. private RestMethod createSpecialMethod(Method method, HttpAttribute httpConfig) { String restMethodName = ""; if (httpConfig.getMethodKind() == MethodKind.NONE) { // Not an HTTP method. Create a dummy rest method. return RestMethod.create(method, RestKind.CUSTOM, "", method.getFullName(), restMethodName); } return null; } // Create a custom rest method. If the last path segment is a literal, it will be used // as the verb for the custom method, otherwise the custom prefix or the rpc's name. static RestMethod createCustomMethod( Method method, HttpAttribute httpConfig, String customNamePrefix) { ImmutableList path = httpConfig.getFlatPath(); PathSegment lastSegment = path.get(path.size() - 1); // Determine base name. String customName = ""; if (lastSegment instanceof LiteralSegment) { customName = ((LiteralSegment) lastSegment).getLiteral(); path = path.subList(0, path.size() - 1); } else { if (method.getModel().getConfigVersion() > 1) { // From version 2 on, we generate a meaningful name here. customName = method.getSimpleName(); } else if (customNamePrefix.isEmpty()){ // Older versions use the prefix or derive from the http method. customName = httpConfig.getMethodKind().toString().toLowerCase(); } } // Prepend prefix. if (!customNamePrefix.isEmpty() && !customName.toLowerCase().startsWith(customNamePrefix.toLowerCase())) { customName = customNamePrefix + ensureUpperCase(customName); } // Ensure effective start is lower case. customName = ensureLowerCase(customName); String restMethodName = ""; CollectionName collectionName = RestAnalyzer.buildCollectionName(path, method.getModel()); return RestMethod.create( method, RestKind.CUSTOM, collectionName, customName, restMethodName); } private static String ensureUpperCase(String name) { if (!name.isEmpty() && Character.isLowerCase(name.charAt(0))) { return Character.toUpperCase(name.charAt(0)) + name.substring(1); } return name; } private static String ensureLowerCase(String name) { if (!name.isEmpty() && Character.isUpperCase(name.charAt(0))) { return Character.toLowerCase(name.charAt(0)) + name.substring(1); } return name; } // Builds the collection name from a path. static CollectionName buildCollectionName(Iterable segments, Model model) { String version = null; String baseName = Joiner.on('.').skipNulls().join(FluentIterable.from(segments).transform( new Function() { @Override public String apply(PathSegment segm) { if (!(segm instanceof LiteralSegment)) { return null; } LiteralSegment literal = (LiteralSegment) segm; if (literal.isTrailingCustomVerb()) { return null; } return literal.getLiteral(); } })); if (Strings.isNullOrEmpty(version)) { version = ApiVersionUtil.extractMajorVersionFromRestName(baseName); baseName = ApiVersionUtil.stripVersionFromRestName(baseName); } return CollectionName.create(baseName, version); } /** * Assigns this RestGroup's short names to its collections and Rest methods. */ private static void assignShortNames(Collection collections) { ImmutableMap> collectionShortNamesByVersion = generateShortNames(collections); for (String version : collectionShortNamesByVersion.keySet()) { for (Map.Entry collectionshortName : collectionShortNamesByVersion.get(version).entrySet()) { String name = collectionshortName.getValue(); CollectionAttribute collection = collectionshortName.getKey(); collection.setName(name); for (RestMethod method : collection.getMethods()) { method.setBaseCollectionName(name); } } } } private static ImmutableMap> generateShortNames( Collection collections) { // Map[version -> Map[name -> collection]] because collection name must be unique within a // given version, but likely is not unique across versions. Map> versionMap = new HashMap<>(); for (CollectionAttribute collection : collections) { String version = collection.getVersionWithDefault(); if (!versionMap.containsKey(version)) { versionMap.put(version, HashBiMap.create()); } String baseName = collection.getBaseName(); String shortName = baseName.substring(baseName.lastIndexOf(".") + 1); insertOrDisambiguate(versionMap.get(version), shortName, collection); } ImmutableMap.Builder> shortNames = new ImmutableMap.Builder<>(); for (Map.Entry> entry : versionMap.entrySet()) { shortNames.put(entry.getKey(), entry.getValue().inverse()); } return shortNames.build(); } /** * Attempts to insert key-value pair into map; disambiguate if the key is already present. */ private static void insertOrDisambiguate(Map map, String key, CollectionAttribute value) { CollectionAttribute oldValue = map.remove(key); if (oldValue == null) { map.put(key, value); return; } insertOrDisambiguate(map, disambiguate(key, value), value); insertOrDisambiguate(map, disambiguate(key, oldValue), oldValue); } /* * Appends an additional token onto the name of the collection. If no more tokens are available, * returns the name unchanged. */ private static String disambiguate(String name, CollectionAttribute collection) { String[] tokens = collection.getBaseName().split("\\."); int level = tokens.length - 2; String candidate = tokens[tokens.length - 1]; for (; level >= 0; level--) { if (candidate.equals(name)) { break; } candidate = appendCamelCase(candidate, tokens[level]); } return level >= 0 ? appendCamelCase(candidate, tokens[level]) : candidate; } private static String appendCamelCase(String s, String toAppend) { if (Strings.isNullOrEmpty(s)) { return toAppend; } return toAppend + s.substring(0, 1).toUpperCase() + s.substring(1); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy