com.google.api.tools.framework.processors.merger.Merger.java Source code

Java tutorial

Introduction

Here is the source code for com.google.api.tools.framework.processors.merger.Merger.java

Source

/*
 * 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.
 *
 * <p>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<Key<?>> requires() {
        return ImmutableList.<Key<?>>of(Resolved.KEY);
    }

    @Override
    public Key<?> establishes() {
        return Merged.KEY;
    }

    @Override
    public boolean run(Model model) {
        int oldErrorCount = model.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());
            Location location = model.getLocationInConfig(api, "name");
            if (iface != null) {
                // Add interface to the roots.
                model.addRoot(iface);
                // Attach api proto to interface.
                iface.setConfig(api);
            } else {
                model.getDiagCollector().addDiag(Diag.error(location, "Cannot resolve api '%s'.", api.getName()));
            }
        }

        List<Set<ConfigAspect>> orderedAspectGroup = sortForMerge(model.getConfigAspects());
        // Merge-in config aspects.
        for (Set<ConfigAspect> aspects : orderedAspectGroup) {
            for (ConfigAspect aspect : aspects) {
                aspect.startMerging();
            }
        }

        for (Set<ConfigAspect> aspects : orderedAspectGroup) {
            new ConfigAspectMerger(aspects).visit(model);
        }

        for (Set<ConfigAspect> 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.getDiagCollector().getErrorCount()) {
            // No new errors produced -- success.
            model.putAttribute(Merged.KEY, new Merged());
            return true;
        }
        return false;
    }

    private void runValidators(Model model) {
        final List<ConfigValidator<? extends Element>> validators = model.getValidators();

        new Visitor() {
            @SuppressWarnings("unchecked")
            @VisitsBefore
            void validate(Element element) {
                final Class<?> elementType = element.getClass();
                Iterable<ConfigValidator<? extends Element>> validatorsToRun = getValidatorsToRun(validators,
                        elementType);
                for (ConfigValidator<? extends Element> validator : validatorsToRun) {
                    ConfigValidator<Element> castedValidator = (ConfigValidator<Element>) validator;
                    castedValidator.run(element);
                }
            }
        }.visit(model);
    }

    private static FluentIterable<ConfigValidator<? extends Element>> getValidatorsToRun(
            List<ConfigValidator<? extends Element>> validators, final Class<?> elementType) {
        return FluentIterable.from(validators).filter(new Predicate<ConfigValidator<? extends Element>>() {
            @Override
            public boolean apply(ConfigValidator<? extends Element> 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.getDiagCollector()
                    .addDiag(Diag.error(location, "Type selector '%s' specified in the config has bad syntax. "
                            + "Valid format is \"<segment>('.' <segment>)*('.' '*')?\"", typeName));
            return;
        }

        List<TypeRef> typeRefs = model.getSymbolTable().lookupMatchingTypes(typeName, kind);

        if (typeRefs == null || typeRefs.isEmpty()) {
            model.getDiagCollector()
                    .addDiag(Diag.error(location,
                            "Cannot resolve additional %s type '%s' specified in the config. Make"
                                    + " sure the name is right and its associated build target was included"
                                    + " in your protobuf build rule.",
                            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<ConfigAspect> orderedAspects;

        private ConfigAspectMerger(Iterable<ConfigAspect> 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<Set<ConfigAspect>> sortForMerge(Iterable<ConfigAspect> aspects) {
        Map<Class<? extends ConfigAspect>, ConfigAspect> aspectsByType = HashBiMap
                .create(Maps.toMap(aspects, new Function<ConfigAspect, Class<? extends ConfigAspect>>() {
                    @Override
                    public Class<? extends ConfigAspect> apply(ConfigAspect aspect) {
                        return aspect.getClass();
                    }
                })).inverse();
        List<Class<? extends ConfigAspect>> visiting = Lists.newArrayList();
        Map<ConfigAspect, Integer> aspectsToLevel = Maps.newLinkedHashMap();
        for (ConfigAspect aspect : aspects) {
            assignLevelToAspect(aspect, aspectsByType, visiting, aspectsToLevel);
        }
        Map<Integer, Set<ConfigAspect>> aspectsByLevel = Maps.newLinkedHashMap();
        for (ConfigAspect aspect : aspectsToLevel.keySet()) {
            Integer aspectLevel = aspectsToLevel.get(aspect);
            if (!aspectsByLevel.containsKey(aspectLevel)) {
                aspectsByLevel.put(aspectLevel, Sets.<ConfigAspect>newLinkedHashSet());
            }
            aspectsByLevel.get(aspectLevel).add(aspect);
        }
        List<Set<ConfigAspect>> 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<Class<? extends ConfigAspect>, ConfigAspect> aspectsByType,
            List<Class<? extends ConfigAspect>> visiting, Map<ConfigAspect, Integer> aspectToLevel) {
        Class<? extends ConfigAspect> 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<? extends ConfigAspect> 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;
    }
}