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

com.google.api.tools.framework.aspects.ConfigRuleSet Maven / Gradle / Ivy

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

import com.google.api.tools.framework.model.ConfigLocationResolver;
import com.google.api.tools.framework.model.Diag;
import com.google.api.tools.framework.model.DiagCollector;
import com.google.api.tools.framework.model.Location;
import com.google.api.tools.framework.model.Model;
import com.google.api.tools.framework.model.ProtoElement;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

/**
 * Represents a configuration rule set, allowing to discover a configuration rule for a given
 * proto element. Helper type to implement config aspects.
 */
public class ConfigRuleSet {
  /**
   * Constructs a rule set for the given type and rules.
   */
  public static  ConfigRuleSet of(
      Descriptor ruleDescriptor, List rules, Model model) {
    return new ConfigRuleSet(ruleDescriptor, rules, model);
  }

  private static final int SELECTOR_FIELD_NUM = 1;
  private static final String SELECTOR_FIELD_NAME = "selector";
  private static final Joiner SELECTOR_JOINER = Joiner.on(',');
  private static final String MAINTAIN_SELECTOR_MINIMIZATION_BUG =
      "maintain_selector_minimization_bug";

  // This pattern is: ('.' )*(('.' '*')|'(.('.' )*)')? OR '*'
  private static final Pattern SELECTOR_PATTERN =
      Pattern.compile("^(\\w+(\\.\\w+)*((\\.\\*)|(\\.\\(\\w+(\\.\\w+)*\\)))?)|\\*$");

  private final List> rules;
  private final Model model;
  private final FieldDescriptor selectorFieldDesc;
  private final Map ruleMap = Maps.newHashMap();

  /**
   * Mapping from rule to unmatched selectors.
   */
  private final Map, Set> unmatchedRules = Maps.newLinkedHashMap();

  public ConfigRuleSet(Descriptor ruleDescriptor, List rules, Model model) {
    Preconditions.checkNotNull(ruleDescriptor);
    Preconditions.checkNotNull(model);
    this.selectorFieldDesc = ruleDescriptor.findFieldByNumber(SELECTOR_FIELD_NUM);
    this.model = model;
    // Sanity check for selector field.
    Preconditions.checkArgument(
        selectorFieldDesc != null
            && selectorFieldDesc.getName().equals(SELECTOR_FIELD_NAME)
            && selectorFieldDesc.getType() == FieldDescriptor.Type.STRING
            && !selectorFieldDesc.isRepeated(),
        "Config rule selector field not present or has unexpected name, type, or cardinality.");

    this.rules = minimize(buildRules(rules));

    for (Rule rule : this.rules) {
      unmatchedRules.put(rule, Sets.newHashSet(rule.selectors));
    }
  }

  /**
   * Returns true if experiment maintain_selector_minimization_bug is enabled; false otherwise.
   */
  private boolean maintainSelectorMinimizationBugExperimentEnabled() {
    return (model != null && model.isExperimentEnabled(MAINTAIN_SELECTOR_MINIMIZATION_BUG));
  }

  /**
   * Build {@link Rule} instances from given rules.
   */
  private List> buildRules(List rules) {
    ImmutableList.Builder> flattened = ImmutableList.builder();
    for (RuleType rule : rules) {
      flattened.add(new Rule(rule));
    }
    return flattened.build();
  }

  /**
   * Validate selector syntax and report errors.
   */
  public void reportBadSelectors(
      DiagCollector collector, ConfigLocationResolver configLocationResolver, String category) {
    reportBadSelectors(collector, configLocationResolver, category, "");
  }

  /**
   * Validate selector syntax and report errors (with given error message prefix).
   */
  public void reportBadSelectors(DiagCollector collector,
      ConfigLocationResolver configLocationResolver, String category, String messagePrefix) {
    for (Rule rule : rules) {
      for (String selector : rule.selectors) {
        if (!SELECTOR_PATTERN.matcher(selector).matches()) {
          collector.addDiag(getBadSelectorErrorDiag(
              configLocationResolver.getLocationInConfig(rule.rule, "selector"), category,
              messagePrefix, selector));
        }
      }
    }
  }

  private Diag getBadSelectorErrorDiag(
      Location location, String category, String messagePrefix, String selector) {
    return Diag.error(location,
        messagePrefix + "%s rule has bad syntax in selector '%s'. See "
        + "documentation for information on selector syntax.",
        category, selector);
  }

  /**
   * Minimize a rule set, removing selectors that are subsumed by selectors of subsequent rules.
   * Rules will be removed if all selectors have been subsumed subsequent rules.
   */
  private List> minimize(List> rules) {
    ImmutableList.Builder> minimized = ImmutableList.builder();
    for (int i = 0; i < rules.size(); i++) {
      Rule rule = rules.get(i);
      rule.minimizeSelectors(rules, i + 1);
      if (!rule.selectors.isEmpty()) {
        minimized.add(rule);
      }
    }
    return minimized.build();
  }

  /**
   * Returns the matching rule for the element, or null, if no matching exists.
   */
  @Nullable
  public RuleType matchingRule(ProtoElement elem) {
    RuleType result = ruleMap.get(elem);
    if (result != null) {
      return result;
    }

    for (int i = rules.size() - 1; i >= 0; i--) {
      Rule rule = rules.get(i);
      String matchedSelector = rule.getMatchedSelector(elem);
      if (matchedSelector != null) {
        ruleMap.put(elem, rule.rule);
        if (unmatchedRules.containsKey(rule)) {
          Set unmatchedSelectors = unmatchedRules.get(rule);
          unmatchedSelectors.remove(matchedSelector);
          if (unmatchedSelectors.isEmpty()) {
            unmatchedRules.remove(rule);
          }
        }
        return rule.rule;
      }
    }
    return null;
  }

  /**
   * Reports any unmatched rules.
   */
  public void reportUnmatchedRules(
      DiagCollector collector, ConfigLocationResolver configLocationResolver, String category) {
    for (Map.Entry, Set> unmatched : unmatchedRules.entrySet()) {
      Set selectors = unmatched.getValue();
      selectors.remove("*");
      // For rules which are not general default, report a warning.
      if (!selectors.isEmpty()) {
        String unmatchedSelectors = SELECTOR_JOINER.join(unmatched.getValue());
        collector.addDiag(Diag.warning(
            configLocationResolver.getLocationInConfig(unmatched.getKey().rule, "selector"),
            "%s rule has selector(s) '%s' that do not match and are not "
            + "shadowed by other rules.",
            category, unmatchedSelectors));
      }
    }
  }

  /**
   * Represent Rule which keeps RuleType with comma delimited selectors Flattened into
   * {@link Iterable} of selectors.
   */
  private class Rule {
    private final Splitter selectorSplitter = Splitter.on(',').trimResults();
    private final RuleType rule;
    private final Set selectors;
    private final FieldDescriptor selectorField;

    private Rule(RuleType rule) {
      this.rule = rule;
      selectorField = rule.getDescriptorForType().findFieldByNumber(SELECTOR_FIELD_NUM);
      List subSelectors = selectorSplitter.splitToList(getUnflattenedSelector());
      // It seems to be a common pattern to end all subselectors with a comma, even if it's the
      // last one. Warnings about the blank selector are ignored, so we remove the blank selector
      // if we see it. We only do it if there are multiple subselectors.
      if (subSelectors.size() > 1 && "".equals(subSelectors.get(subSelectors.size() - 1).trim())) {
        subSelectors = subSelectors.subList(0, subSelectors.size() - 1);
      }
      this.selectors = Sets.newHashSet(subSelectors);
    }

    /**
     * Returns the unflattened selectors of the rule.
     */
    private String getUnflattenedSelector() {
      return (String) rule.getField(selectorField);
    }

    /**
     * Returns the selector that matches full name of given {@link ProtoElement}.
     * Otherwise, returns null.
     */
    private String getMatchedSelector(ProtoElement elem) {
      for (String selector : selectors) {
        if (matches(selector, elem.getFullName())) {
          return selector;
        }
      }
      return null;
    }

    /**
     * Check whether a name matches selector.
     */
    private boolean matches(String selector, String name) {
      if (selector.equals("*")) {
        return true;
      }
      if (selector.endsWith(".*")) {
        return name.startsWith(selector.substring(0, selector.length() - 1));
      }
      return name.equals(selector);
    }

    /**
     * Remove selectors if they are subsumed by any selectors of given rule list.
     */
    private void minimizeSelectors(List> rules, int startIndex) {
      for (Iterator iter = selectors.iterator(); iter.hasNext();) {
        String selector = iter.next();
        for (int i = startIndex; i < rules.size(); i++) {
          if (isSubsumed(selector, rules.get(i).selectors)) {
            iter.remove();
            break;
          }
        }
      }
    }

    /**
     * Check whether selector is subsumed by any of other selectors.
     */
    private boolean isSubsumed(String selector, Iterable others) {
      for (String other : others) {
        if (subsumes(other, selector)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Check whether selector subsumes another selector, i.e. every name matching the
     * other selector will also match this selector.
     */
    private boolean subsumes(String selector, String other) {
      if (selector.equals("*")) {
        return true;
      }
      if (other.equals("*")) {
        return false;
      }
      if (selector.endsWith(".*")) {
        selector = selector.substring(0, selector.length() - 1);
        if (other.endsWith(".*")) {
          other = other.substring(0, other.length() - 1);
        }
        if (ConfigRuleSet.this.maintainSelectorMinimizationBugExperimentEnabled()) {
          return selector.startsWith(other);
        } else {
          return other.startsWith(selector);
        }
      }
      return selector.equals(other);
    }
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy