com.google.api.tools.framework.processors.merger.Merger 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.processors.merger;
import com.google.api.Service;
import com.google.api.tools.framework.aspects.visibility.model.ScoperImpl;
import com.google.api.tools.framework.model.ConfigAspect;
import com.google.api.tools.framework.model.ConfigValidator;
import com.google.api.tools.framework.model.Diag;
import com.google.api.tools.framework.model.Element;
import com.google.api.tools.framework.model.Interface;
import com.google.api.tools.framework.model.Location;
import com.google.api.tools.framework.model.Model;
import com.google.api.tools.framework.model.Processor;
import com.google.api.tools.framework.model.ProtoElement;
import com.google.api.tools.framework.model.TypeRef;
import com.google.api.tools.framework.model.Visitor;
import com.google.api.tools.framework.model.stages.Merged;
import com.google.api.tools.framework.model.stages.Resolved;
import com.google.api.tools.framework.util.VisitsBefore;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Key;
import com.google.protobuf.Api;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* A processor which establishes the {@link Merged} stage, in which service config and IDL are
* combined and validated.
*
* The merger also derives interpreted values from the configuration, for example, the {@link
* com.google.api.tools.framework.aspects.http.HttpConfigAspect}, and reports consistency errors
* encountered during interpretation.
*/
public class Merger implements Processor {
private static final Pattern SELECTOR_PATTERN = Pattern.compile("^(\\w+(\\.\\w+)*(\\.\\*)?)$");
@Override
public ImmutableList> requires() {
return ImmutableList.>of(Resolved.KEY);
}
@Override
public Key establishes() {
return Merged.KEY;
}
@Override
public boolean run(Model model) {
int oldErrorCount = model.getDiagReporter().getDiagCollector().getErrorCount();
if (model.getServiceConfig() == null) {
// No service config defined; create a dummy one.
model.setServiceConfig(Service.getDefaultInstance());
}
// Resolve apis, computing which parts of the model are included. Attach apis to interfaces.
Service config = model.getServiceConfig();
for (Api api : model.getServiceConfig().getApisList()) {
Interface iface = model.getSymbolTable().lookupInterface(api.getName());
if (iface != null) {
// Add interface to the roots.
model.addRoot(iface);
// Attach api proto to interface.
iface.setConfig(api);
} else {
Location location = model.getLocationInConfig(api, "name");
model
.getDiagReporter()
.report(Diag.error(location, "Cannot resolve api '%s'.", api.getName()));
}
}
List> orderedAspectGroup = sortForMerge(model.getConfigAspects());
// Merge-in config aspects.
for (Set aspects : orderedAspectGroup) {
for (ConfigAspect aspect : aspects) {
aspect.startMerging();
}
}
for (Set aspects : orderedAspectGroup) {
new ConfigAspectMerger(aspects).visit(model);
}
for (Set aspects : orderedAspectGroup) {
for (ConfigAspect aspect : aspects) {
aspect.endMerging();
}
}
runValidators(model);
// Resolve types and enums specified in the service config as additional inclusions to
// the tool chain, but not reachable from the service IDL, such as types associated with
// Any type.
for (com.google.protobuf.Type type : config.getTypesList()) {
addAdditionalType(
model, model.getLocationInConfig(type, "name"), type.getName(), Type.TYPE_MESSAGE);
}
for (com.google.protobuf.Enum enumType : config.getEnumsList()) {
addAdditionalType(
model, model.getLocationInConfig(enumType, "name"), enumType.getName(), Type.TYPE_ENUM);
}
// Set the initial scoper based on the roots. This will scope down further operation on the
// model to those elements reachable via the roots.
model.setScoper(ScoperImpl.create(model.getRoots()));
if (oldErrorCount == model.getDiagReporter().getDiagCollector().getErrorCount()) {
// No new errors produced -- success.
model.putAttribute(Merged.KEY, new Merged());
return true;
}
return false;
}
private void runValidators(Model model) {
final List> validators = model.getValidators();
new Visitor() {
@SuppressWarnings("unchecked")
@VisitsBefore
void validate(Element element) {
final Class elementType = element.getClass();
Iterable> validatorsToRun =
getValidatorsToRun(validators, elementType);
for (ConfigValidator validator : validatorsToRun) {
ConfigValidator castedValidator = (ConfigValidator) validator;
castedValidator.run(element);
}
}
}.visit(model);
}
private static FluentIterable> getValidatorsToRun(
List> validators, final Class elementType) {
return FluentIterable.from(validators)
.filter(
new Predicate>() {
@Override
public boolean apply(ConfigValidator validator) {
return validator.getElementClass().isAssignableFrom(elementType);
}
});
}
/**
* Resolve the additional type specified besides those that can be reached transitively from
* service definition. It resolves the typeName into a {@link TypeRef} object. If typeName ends
* with wildcard ".*", all the {@link TypeRef}s that is under typeName pattern path are added to
* the root.
*/
private void addAdditionalType(
Model model, Location location, final String typeName, final Type kind) {
if (!SELECTOR_PATTERN.matcher(typeName).matches()) {
model
.getDiagReporter()
.report(
Diag.error(
location,
"Type selector '%s' specified in the config has bad syntax. "
+ "Valid format is \"('.' )*('.' '*')?\"",
typeName));
return;
}
List typeRefs = model.getSymbolTable().lookupMatchingTypes(typeName, kind);
if (typeRefs == null || typeRefs.isEmpty()) {
model
.getDiagReporter()
.report(
Diag.error(
location,
"Cannot resolve additional %s type '%s' specified in the config.",
kind,
typeName));
} else {
for (TypeRef typeRef : typeRefs) {
if (typeRef.isMessage()) {
model.addRoot(typeRef.getMessageType());
} else if (typeRef.isEnum()) {
model.addRoot(typeRef.getEnumType());
}
}
}
}
private static class ConfigAspectMerger extends Visitor {
private final Iterable orderedAspects;
private ConfigAspectMerger(Iterable orderedAspects) {
this.orderedAspects = orderedAspects;
}
@VisitsBefore
void merge(ProtoElement element) {
for (ConfigAspect aspect : orderedAspects) {
aspect.merge(element);
}
}
}
/**
* Returns the given config aspects as list of group of aspects in merge dependency order. This
* performs a 'longest path layering' algorithm by placing aspects at different levels (layers).
* First place all sink nodes at level-1 and then each node n is placed at level level-p+1, where
* p is the longest path from n to sink. Aspects in each level are independent of each other and
* can only depend on aspects in lower levels. Detailed algorithm : 13.3.2 Layer Assignment
* Algorithms : https://cs.brown.edu/~rt/gdhandbook/chapters/hierarchical.pdf
*/
private static List> sortForMerge(Iterable aspects) {
Map, ConfigAspect> aspectsByType =
HashBiMap.create(
Maps.toMap(
aspects,
new Function>() {
@Override
public Class apply(ConfigAspect aspect) {
return aspect.getClass();
}
}))
.inverse();
List> visiting = Lists.newArrayList();
Map aspectsToLevel = Maps.newLinkedHashMap();
for (ConfigAspect aspect : aspects) {
assignLevelToAspect(aspect, aspectsByType, visiting, aspectsToLevel);
}
Map> aspectsByLevel = Maps.newLinkedHashMap();
for (ConfigAspect aspect : aspectsToLevel.keySet()) {
Integer aspectLevel = aspectsToLevel.get(aspect);
if (!aspectsByLevel.containsKey(aspectLevel)) {
aspectsByLevel.put(aspectLevel, Sets.newLinkedHashSet());
}
aspectsByLevel.get(aspectLevel).add(aspect);
}
List> aspectListByLevels = Lists.newArrayList();
for (int level = 1; level <= aspectsByLevel.size(); ++level) {
aspectListByLevels.add(aspectsByLevel.get(level));
}
return aspectListByLevels;
}
/**
* Does a DFS traversal and computes the maximum height (level) of each node from the sink node.
*/
private static int assignLevelToAspect(
ConfigAspect aspect,
Map, ConfigAspect> aspectsByType,
List> visiting,
Map aspectToLevel) {
Class aspectType = aspect.getClass();
if (aspectToLevel.containsKey(aspect)) {
return aspectToLevel.get(aspect);
}
if (visiting.contains(aspectType)) {
throw new IllegalStateException(
String.format(
"Cyclic dependency between config aspect attributes. Cycle is: %s <- %s",
aspectType, Joiner.on(" <- ").join(visiting)));
}
visiting.add(aspectType);
Integer childMaxHeight = 0;
for (Class dep : aspect.mergeDependencies()) {
if (aspectsByType.containsKey(dep)) {
Integer childHeight =
assignLevelToAspect(aspectsByType.get(dep), aspectsByType, visiting, aspectToLevel);
childMaxHeight = childHeight > childMaxHeight ? childHeight : childMaxHeight;
} else {
throw new IllegalStateException(
String.format(
"config aspect %s depends on an unregistered aspect %s.",
aspectType.getSimpleName(), dep.getSimpleName()));
}
}
visiting.remove(aspectType);
aspectToLevel.put(aspect, childMaxHeight + 1);
return childMaxHeight + 1;
}
}