@Unstable
@Named
public interface MetricEntityAttributeDescriptor {

  /**
   * Returns the name of the attribute. This name uniquely identifies this
   * attribute and is used to reference the metric in the Cloudera Manager
   * API and charting features.
   * @return
   */
  @MetricEntityAttributeNameFormat
  String getName();

  /**
   * Returns the display name of the attribute.
   * @return
   */
  @NotEmpty
  String getLabel();

  /**
   * Returns the description of the attribute.
   * @return
   */
  @NotEmpty
  String getDescription();

  /**
   * Returns whether to treat attribute values as case-sensitive.
   * @return
   */
  boolean isValueCaseSensitive();
} @Named
@Unstable
public interface MetricEntityTypeDescriptor {

  /**
   * Returns the name of the entity type. This name uniquely identifies this
   * entity type and is used to reference the type in the Cloudera Manager
   * API and charting features.
   * @return
   */
  @MetricEntityTypeFormat
  String getName();

  /**
   * Returns the string to use to pluralize the name of the entity for cross
   * entity aggregate metrics. For example, for an entity named "ECHO_ENTITY"
   * this should be "echo_entities". Cross entity aggregate metric names will be
   * composed using this to generate metrics named like
   * 'fd_open_across_echo_entities'.
   *
   * The string must be prefixed with the service name and be unique within this
   * descriptor.
   *
   * If this is not specified the name will be constructed from the service
   * name, the entity name, and an "s".
   * @return
   */
  @NameForCrossEntityAggregatesFormat
  String getNameForCrossEntityAggregateMetrics();

  /**
   * Returns the display name of the entity type.
   * @return
   */
  @NotEmpty
  String getLabel();

  /**
   * Returns the display name of the entity type in plural form.
   * @return
   */
  @NotEmpty
  String getLabelPlural();

  /**
   * Returns the description of the entity type.
   * @return
   */
  @NotEmpty
  String getDescription();

  /**
   * Returns the list immutable attributes for this entity type. Immutable
   * attributes values for an entity may not change over its lifetime.
   * @return
   */
  @NotEmpty
  List<String> getImmutableAttributeNames();

  /**
   * Returns the list mutable attributes for this entity type. Mutable
   * attributes for an entity may change over its lifetime.
   * @return
   */
  List<String> getMutableAttributeNames();

  /**
   * Returns a list of attribute names that will be used to construct entity
   * names for entities of this type. The attributes named here must be immutable
   * attributes of this type or a parent type.
   * @return
   */
  @NotEmpty
  List<String> getEntityNameFormat();

  /**
   * Returns a format string that will be used to construct the display name of
   * entities of this type. If this returns null the entity name would be used
   * as the display name.
   *
   * The entity attribute values are used to replace $attribute name portions of
   * this format string. For example, an entity with roleType "DATANODE" and
   * hostname "foo.com" will have a display name "DATANODE (foo.com)" if the
   * format is "$roleType ($hostname)".
   * @return
   */
  String getEntityLabelFormat();

  /**
   * Returns a list of metric entity type names which are parents of this
   * metric entity type. A metric entity type inherits the attributes of
   * its ancestors. For example a role metric entity type has its service as a
   * parent. A service metric entity type has a cluster as a parent. The role
   * type inherits its cluster name attribute through its service parent. Only
   * parent ancestors should be returned here. In the example given, only the
   * service metric entity type should be specified in the parent list.
   */
  List<String> getParentMetricEntityTypeNames();

  /**
   * Specifies metrics that will be registered for this metric entity type.
   * @return
   */
  @UniqueField("name")
  List<MetricDescriptor> getMetricDefinitions();
} @Named
@Unstable
public interface RoleMonitoringDefinitionsDescriptor {

  /**
   * The name of the role. If this descriptor is part of a CSD bundle this
   * should match the role name in one of the {@RoleDescriptor}.
   * @return
   */
  @EntityTypeFormat
  String getName();

  /**
   * Optional. If set, specifies metrics that will be registered for this role.
   * These are in addition to default role metrics defined for all roles by
   * Cloudera Manager.
   * @return
   */
  @Valid
  @UniqueField("name")
  List<MetricDescriptor> getMetricDefinitions();

  /**
   * Returns the string to use to pluralize the name of the role for cross
   * entity aggregate metrics. For example, for a role named "ECHO_PUTTY"
   * this should be "echo_putties". Cross entity aggregate metric names will be
   * composed using this to generate metrics named like
   * 'fd_open_across_echo_putties'.
   *
   * The string must be prefixed with the service name and be unique within this
   * descriptor.
   *
   * If this is not specified the name will be constructed from the service
   * name, the role name, and an "s".
   * @return
   */
  @Valid
  @NameForCrossEntityAggregatesFormat
  String getNameForCrossEntityAggregateMetrics();

  /**
   * Returns the list of additional immutable attributes for this role. See the
   * getMutableAttributeNames call in MetricEntityTypeDescriptor for more on
   * mutable attributes.
   * @return
   */
  List<String> getAdditionalImmutableAttributeNames();

  /**
   * Returns the list of additional mutable attributes for this role. See the
   * getMutableAttributeNames call in MetricEntityTypeDescriptor for more on
   * mutable attributes.
   * @return
   */
  List<String> getAdditionalMutableAttributeNames();
} public abstract class AbstractMonitoringValidator<V> {

  /**
   * Returns a string describing what the validator validates to be used in
   * documentation.
   * @return
   */
  public abstract String getDescription();

  /**
   * Validate the node returning a list of non-empty ConstraintViolation<T> if a
   * validation failure was encountered, empty list if no violations were found.
   *
   * Implementations can assume that they are called iff all annotation based
   * validations have passed. For example, implementations do not need to check
   * if 'node' is 'null' if 'node' is annotated with @NotNull.
   * @param context
   * @param node
   * @param path
   * @return
   */
  public abstract <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      V node,
      DescriptorPathImpl path);

  /**
   * Readability method.
   * @param <T>
   * @return
   */
  protected static <T> ImmutableList<T> noViolations() {
    return ImmutableList.of();
  }

  /**
   * Return an adjusted path for the property being validated for the node.
   * @param node
   * @param propertyName
   * @param path
   * @return
   */
  protected DescriptorPathImpl constructPathFromProperty(
      V node,
      String propertyName,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(node);
    Preconditions.checkNotNull(propertyName);
    Preconditions.checkNotNull(path);
    return AbstractMonitoringValidator.getPathFromProperty(node,
                                                           propertyName,
                                                           path);
  }

  /**
   * Returns an immutable list containing exactly one violation for the bean
   * 'currentBean' with value 'currentValue' at path 'path' with 'message'.
   * @param message
   * @param currentBean
   * @param currentValue
   * @param path
   * @param <T>
   * @param <V>
   * @return
   */
  protected static <T, V> List<ConstraintViolation<T>> forViolation(
      String message,
      V currentBean,
      Object currentValue,
      DescriptorPathImpl path) {
    ConstraintViolation<T> constraintViolation =
          MonitoringConstraintViolation.forViolation(message,
                                                     currentBean,
                                                     currentValue,
                                                     path);
      return ImmutableList.of(constraintViolation);
  }

  /**
   * Return an adjusted path for the property being validated for the node.
   * @param node
   * @param propertyName
   * @param path
   * @return
   */
  public static <T> DescriptorPathImpl getPathFromProperty(
      T node,
      String propertyName,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(node);
    Preconditions.checkNotNull(propertyName);
    Preconditions.checkNotNull(path);
    Method methodOfPropertyValidated =
        ReflectionHelper.propertyGetter(node, propertyName);
    Preconditions.checkNotNull(methodOfPropertyValidated);
    return path.addPropertyNode(methodOfPropertyValidated, false);
  }

  public static <T> void safeAddAllToCollection(Collection<T> target,
                                                Collection<T> source) {
    Preconditions.checkNotNull(target);
    if (null != source) {
      target.addAll(source);
    }
  }
} public class MonitoringValidationContext {

  public final ServiceMonitoringDefinitionsDescriptor serviceDescriptor;
  public final ImmutableMap<String, MetricDescriptor> metricsDefined;
  public final ImmutableMap<String, MetricEntityTypeDescriptor> entitiesDefined;
  public final ImmutableMap<String, RoleMonitoringDefinitionsDescriptor> rolesDefined;
  public final ImmutableMap<String, MetricEntityAttributeDescriptor> attributesDefined;

  public MonitoringValidationContext(
      ServiceMonitoringDefinitionsDescriptor serviceDescriptor) {
    Preconditions.checkNotNull(serviceDescriptor);
    this.serviceDescriptor = serviceDescriptor;
    ImmutableMap.Builder<String, RoleMonitoringDefinitionsDescriptor>
      rolesDefinedBuilder = ImmutableMap.builder();
    // We can't use an ImmutableMap.Builder since it will not allow multiple
    // entries with the same key. Instead we build a local hash map and make
    // it immutable below.
    Map<String, MetricDescriptor> metricsDefinedBuilder = Maps.newHashMap();
    metricsDefinedBuilder.putAll(
        extractMetrics(serviceDescriptor.getMetricDefinitions()));
    if (null != serviceDescriptor.getRoles()) {
      for (RoleMonitoringDefinitionsDescriptor role :
           serviceDescriptor.getRoles()) {
        rolesDefinedBuilder.put(
            MonitoringConventions.getRoleMetricEntityTypeName(
                serviceDescriptor.getName(),
                role.getName()),
            role);
        metricsDefinedBuilder.putAll(
            extractMetrics(role.getMetricDefinitions()));
      }
    }
    if (null != serviceDescriptor.getMetricEntityTypeDefinitions()) {
      for (MetricEntityTypeDescriptor entity :
          serviceDescriptor.getMetricEntityTypeDefinitions()) {
        metricsDefinedBuilder.putAll(
            extractMetrics(entity.getMetricDefinitions()));
      }
    }
    ImmutableMap.Builder<String, MetricEntityTypeDescriptor>
      entitiesDefinedBuilder = ImmutableMap.builder();
    if (null != serviceDescriptor.getMetricEntityTypeDefinitions()) {
      for (MetricEntityTypeDescriptor entity :
           serviceDescriptor.getMetricEntityTypeDefinitions()) {
        entitiesDefinedBuilder.put(entity.getName(), entity);
      }
    }
    ImmutableMap.Builder<String, MetricEntityAttributeDescriptor>
      attributesDefinedBuilder = ImmutableMap.builder();
    if (null != serviceDescriptor.getMetricEntityAttributeDefinitions()) {
      for (MetricEntityAttributeDescriptor attribute :
           serviceDescriptor.getMetricEntityAttributeDefinitions()) {
        attributesDefinedBuilder.put(attribute.getName(), attribute);
      }
    }
    metricsDefined = ImmutableMap.copyOf(metricsDefinedBuilder);
    rolesDefined = rolesDefinedBuilder.build();
    entitiesDefined = entitiesDefinedBuilder.build();
    attributesDefined = attributesDefinedBuilder.build();
  }

  private Map<String, MetricDescriptor> extractMetrics(
      @Nullable List<MetricDescriptor> metrics) {
    if (null == metrics) {
      return ImmutableMap.of();
    }
    Map<String, MetricDescriptor> ret = Maps.newHashMap();
    for (MetricDescriptor metric : metrics) {
      ret.put(metric.getName(), metric);
    }
    return ret;
  }
} public class AdditionalAttributesReferToExistingAttributesValidator
    extends AbstractMonitoringValidator<RoleMonitoringDefinitionsDescriptor> {

  private final ImmutableSet<String> builtInAttributes;

  public AdditionalAttributesReferToExistingAttributesValidator(
      Set<String> builtInAttributes) {
    Preconditions.checkNotNull(builtInAttributes);
    this.builtInAttributes = ImmutableSet.copyOf(builtInAttributes);
  }

  @Override
  public String getDescription() {
    return
        "Validates that additional immutable and mutable attribute names " +
        "refer to either attributes defined in this descriptor or built-in " +
        "attribute names.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      RoleMonitoringDefinitionsDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);

    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    ret.addAll(this.<T>checkAttributes(context,
                                       entity.getAdditionalImmutableAttributeNames(),
                                       "additionalImmutableAttributeNames",
                                       entity,
                                       path));
    ret.addAll(this.<T>checkAttributes(context,
                                       entity.getAdditionalMutableAttributeNames(),
                                       "additionalMutableAttributeNames",
                                       entity,
                                       path));
    return ret;
  }

  private <T> List<ConstraintViolation<T>> checkAttributes(
      MonitoringValidationContext context,
      @Nullable List<String>attributes,
      String attributesSourceName,
      RoleMonitoringDefinitionsDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(attributesSourceName);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);
    if (null == attributes) {
      return noViolations();
    }
    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    path = constructPathFromProperty(entity,
                                     attributesSourceName,
                                     path);
    for (String attribute : attributes) {
      if (!context.attributesDefined.containsKey(attribute) &&
          !builtInAttributes.contains(attribute)) {
        String msg = String.format(
            "Unknown attribute '%s' for metric entity type '%s'.",
             attribute,
             entity.getName());
        ret.addAll(
            AbstractMonitoringValidator.<T, RoleMonitoringDefinitionsDescriptor>
                forViolation(msg, entity, attribute, path));
      }
    }
    return ret;
  }
} public class AttributeNamePrefixedWithServiceNameValidator extends
    AbstractMonitoringValidator<MetricEntityAttributeDescriptor> {

  private final ImmutableSet<String> builtInAttributes;

  public AttributeNamePrefixedWithServiceNameValidator(
      Set<String> builtInAttributes) {
    Preconditions.checkNotNull(builtInAttributes);
    this.builtInAttributes = ImmutableSet.copyOf(builtInAttributes);
  }

  @Override
  public String getDescription() {
    return
        "Validates that the name of the MetricEntityAttributeDescriptor " +
        "starts with the service name (in small caps). This ensures that " +
        "attributes names are unique across services.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricEntityAttributeDescriptor attribute,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(attribute);
    Preconditions.checkNotNull(path);
    path = constructPathFromProperty(attribute, "name", path);
    String attributeName = attribute.getName();
    String serviceName = context.serviceDescriptor.getName().toLowerCase();
    if (!attributeName.startsWith(serviceName) &&
        !builtInAttributes.contains(attributeName)) {
      String msg = String.format(
          "Attribute '%s' does not start with the service name ('%s')",
          attributeName,
          serviceName);
      return forViolation(msg, attribute, attributeName, path);
    }
    return noViolations();
  }
} public class AttributesReferToExistingAttributesValidator
    extends AbstractMonitoringValidator<MetricEntityTypeDescriptor> {

  private final ImmutableSet<String> builtInAttributes;

  public AttributesReferToExistingAttributesValidator(
      Set<String> builtInAttributes) {
    Preconditions.checkNotNull(builtInAttributes);
    this.builtInAttributes = ImmutableSet.copyOf(builtInAttributes);
  }

  @Override
  public String getDescription() {
    return
        "Validates that immutable and mutable attribute names refer to either " +
        "attributes defined in this descriptor or built-in attribute names.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricEntityTypeDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);

    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    ret.addAll(this.<T>checkAttributes(context,
                                       entity.getImmutableAttributeNames(),
                                       "immutableAttributeNames",
                                       entity,
                                       path));
    ret.addAll(this.<T>checkAttributes(context,
                                       entity.getMutableAttributeNames(),
                                       "mutableAttributeNames",
                                       entity,
                                       path));
    return ret;
  }

  private <T> List<ConstraintViolation<T>> checkAttributes(
      MonitoringValidationContext context,
      @Nullable List<String>attributes,
      String attributesSourceName,
      MetricEntityTypeDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(attributesSourceName);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);
    if (null == attributes) {
      return noViolations();
    }
    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    path = constructPathFromProperty(entity,
                                     attributesSourceName,
                                     path);
    for (String attribute : attributes) {
      if (!context.attributesDefined.containsKey(attribute) &&
          !builtInAttributes.contains(attribute)) {
        String msg = String.format(
            "Unknown attribute '%s' for metric entity type '%s'.",
             attribute,
             entity.getName());
        ret.addAll(
            AbstractMonitoringValidator.<T, MetricEntityTypeDescriptor>
                forViolation(msg, entity, attribute, path));
      }
    }
    return ret;
  }
} public class ConsistentMetricDefinitionValidator
    extends AbstractMonitoringValidator<MetricDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that metric definitions are consistent if that metric is " +
        "defined for multiple entity types.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricDescriptor metricDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metricDescriptor);
    Preconditions.checkNotNull(path);

    MetricDescriptor definition =
        context.metricsDefined.get(metricDescriptor.getName());
    Preconditions.checkNotNull(definition);

    if (!StringUtils.equals(
        metricDescriptor.getLabel(),
        definition.getLabel())) {
      String msg = String.format(
          "Inconsistent labels for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.getLabel(),
          definition.getLabel());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.getLabel(), path);
    }

    if (!StringUtils.equals(
        metricDescriptor.getDescription(),
        definition.getDescription())) {
      String msg = String.format(
          "Inconsistent descriptions for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.getDescription(),
          definition.getDescription());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.getDescription(), path);
    }

    if (!StringUtils.equals(
        metricDescriptor.getNumeratorUnit(),
        definition.getNumeratorUnit())) {
      String msg = String.format(
          "Inconsistent numerator units for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.getNumeratorUnit(),
          definition.getNumeratorUnit());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.getNumeratorUnit(), path);
    }

    if (!StringUtils.equals(
        metricDescriptor.getDenominatorUnit(),
        definition.getDenominatorUnit())) {
      String msg = String.format(
          "Inconsistent denominator units for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.getDenominatorUnit(),
          definition.getDenominatorUnit());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.getDenominatorUnit(), path);
    }

    if (metricDescriptor.isCounter() != definition.isCounter()) {
      String msg = String.format(
          "Inconsistent counter definitions for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.isCounter(),
          definition.isCounter());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.isCounter(), path);
    }

    if (!StringUtils.equals(
        metricDescriptor.getWeightingMetricName(),
        definition.getWeightingMetricName())) {
      String msg = String.format(
          "Inconsistent weighting metric names for metric '%s': '%s' and '%s'. ",
          metricDescriptor.getName(),
          metricDescriptor.getWeightingMetricName(),
          definition.getWeightingMetricName());
        return forViolation(
            msg, metricDescriptor, metricDescriptor.getWeightingMetricName(), path);
    }

    return noViolations();
  }
} public class CounterMetricNameValidator
    extends AbstractMonitoringValidator<MetricDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that counter metrics conform to certain naming convention. " +
        "Cloudera Manager exposes counters as rates and converts the original " +
        "metric name, foo_bar or foo_bar_count, to foo_bar_rate. This " +
        "validator makes sure that the transformation is possible with given " +
        "metric name.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricDescriptor metricDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metricDescriptor);
    if (!metricDescriptor.isCounter()) {
      return noViolations();
    }
    path = constructPathFromProperty(metricDescriptor, "name", path);
    String metricName = metricDescriptor.getName();
    if (!MonitoringConventions.isValidEndingForCounterMetric(metricName)) {
       String msg = String.format(
          "Counter metric '%s' ends with " + MonitoringConventions.RATE_SUFFIX,
          metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }

    String userVisibleName =
        MonitoringConventions.convertCounterNameToUserFacingReadName(
            true,
            metricName);
    if (!MonitoringConventions.isValidMetricNameFormat(userVisibleName)) {
       String msg = String.format(
          "User visible name %s for counter metric '%s' does not a valid format",
          userVisibleName,
          metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }

    if (context.metricsDefined.containsKey(userVisibleName)) {
       String msg = String.format(
          "User visible name %s for counter metric %s collides with a metric " +
          "defined in the MDL with the same name",
          userVisibleName,
          metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }
    return noViolations();
  }

} public class DenominatorValidator
    extends AbstractMonitoringValidator<MetricDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that the metric has a valid denominator. Metrics with " +
        "names ending with " + MonitoringConventions.RATE_SUFFIX + " are " +
        "assumed to be specifying a rate and need to have a denominator. " +
        "Counter metrics should never have denominators.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricDescriptor metricDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metricDescriptor);
    Preconditions.checkNotNull(path);
    path = constructPathFromProperty(metricDescriptor, "name", path);
    String metricName = metricDescriptor.getName();
    String denominatorUnit = metricDescriptor.getDenominatorUnit();
    if (!MonitoringConventions.isValidDenominatorForMetricWithRateEnding(
        metricName,
        denominatorUnit)) {
      String msg = String.format(
        "Non-counter metric '%s' ends with " +
        MonitoringConventions.RATE_SUFFIX + " but has no denominator",
        metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }

    if (metricDescriptor.isCounter() &&
        !MonitoringConventions.isValidDenominatorForCounterMetric(
        denominatorUnit)) {
      String msg = String.format(
        "Counter metric '%s' has a denominator and should not",
        metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }
    return noViolations();
  }

} public class EntityNamePrefixedWithServiceNameValidator extends
    AbstractMonitoringValidator<MetricEntityTypeDescriptor> {

  private final ImmutableSet<String> builtInEntityTypes;

  public EntityNamePrefixedWithServiceNameValidator(
      ImmutableSet<String> builtInEntityTypes) {
    Preconditions.checkNotNull(builtInEntityTypes);
    this.builtInEntityTypes = builtInEntityTypes;
  }

  @Override
  public String getDescription() {
    return
        "Validates that the name of the MetricEntityTypeDescriptor starts " +
        "with the service name followed by an underscore ('_'). This ensures " +
        "that entity names are unique across services.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricEntityTypeDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);
    path = constructPathFromProperty(entity, "name", path);
    String entityName = entity.getName();
    String serviceName =
        context.serviceDescriptor.getName().toUpperCase() + "_";
    if (!entityName.startsWith(serviceName) &&
        !builtInEntityTypes.contains(entityName)) {
      String msg = String.format(
          "Entity '%s' does not start with the service name ('%s')",
          entityName,
          serviceName);
      return forViolation(msg, entity, entityName, path);
    }
    return noViolations();
  }
} public class EntityParentsReferToExistingEntitiesValidator extends
    AbstractMonitoringValidator<MetricEntityTypeDescriptor> {

  private final ImmutableSet<String> builtInRoleTypes;
  private final ImmutableSet<String> builtInEntityTypes;

  public EntityParentsReferToExistingEntitiesValidator(
      ImmutableSet<String> builtInRoleTypes,
      ImmutableSet<String> builtInEntityTypes) {
    this.builtInRoleTypes = Preconditions.checkNotNull(builtInRoleTypes);
    this.builtInEntityTypes = Preconditions.checkNotNull(builtInEntityTypes);
  }

  @Override
  public String getDescription() {
    return
        "Validates that the parents defined for an entity refer to other types " +
        "defined in this descriptor: another entity, a role, or the service.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricEntityTypeDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);
    if (null == entity.getParentMetricEntityTypeNames() ||
        entity.getParentMetricEntityTypeNames().isEmpty()) {
      return noViolations();
    }
    Set<String> entitiesNames =
        Sets.newHashSet(context.entitiesDefined.keySet());
    entitiesNames.add(
        context.serviceDescriptor.getName().toUpperCase());
    entitiesNames.addAll(context.rolesDefined.keySet());
    path = constructPathFromProperty(entity,
                                     "parentMetricEntityTypeNames",
                                     path);
    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    for (String parentName : entity.getParentMetricEntityTypeNames()) {
      if (!entitiesNames.contains(parentName) &&
          !builtInRoleTypes.contains(parentName) &&
          !builtInEntityTypes.contains(parentName)) {
        String msg = String.format(
            "Unknown parent '%s' for metric entity type '%s'.",
            parentName,
            entity.getName());
        ret.addAll(
            AbstractMonitoringValidator.<T, MetricEntityTypeDescriptor>
                forViolation(msg, entity, parentName, path));
      }
    }
    return ret;
  }
} public class MetricNamePrefixedWithServiceNameValidator extends
    AbstractMonitoringValidator<MetricDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that the name of the MetricDescriptor starts with the " +
        "service name followed by an underscore ('_'). This ensures that " +
        "metric names are unique across services. The validator assumes that " +
        "metric names conforms with the metric name format, e.g., the " +
        "names are in all small caps.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricDescriptor metricDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metricDescriptor);
    Preconditions.checkNotNull(path);
    path = constructPathFromProperty(metricDescriptor, "name", path);
    String metricName = metricDescriptor.getName();
    String serviceName =
        context.serviceDescriptor.getName().toLowerCase() + "_";
    if (!metricName.startsWith(serviceName)) {
      String msg = String.format(
          "Metric '%s' does not start with the service name",
          metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, metricName, path);
    }
    return noViolations();
  }
} public class NameForCrossEntityAggregatesIsUniqueValidator
    extends AbstractMonitoringValidator<ServiceMonitoringDefinitionsDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that all the names for cross entity aggregates are unique. " +
        "That is, no two entities (e.g., two roles, the service and an " +
        "entity) have the same name for cross entity aggregates.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      ServiceMonitoringDefinitionsDescriptor serviceDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(serviceDescriptor);
    Preconditions.checkNotNull(path);
    List<ConstraintViolation<T>> violations = Lists.newArrayList();

    Set<String> namesForCrossEntityAggregatesSeen = Sets.newHashSet();
    if (null != serviceDescriptor.getNameForCrossEntityAggregateMetrics()) {
      namesForCrossEntityAggregatesSeen.add(
          serviceDescriptor.getNameForCrossEntityAggregateMetrics());
    }

    if (null != serviceDescriptor.getRoles()) {
      for (RoleMonitoringDefinitionsDescriptor role :
           serviceDescriptor.getRoles()) {
        if (null != role.getNameForCrossEntityAggregateMetrics() &&
            namesForCrossEntityAggregatesSeen.contains(
                role.getNameForCrossEntityAggregateMetrics())) {
          path = path.addBeanNode(role);
          String msg = String.format(
            "'%s' name for cross entity aggregates is already in use",
            role.getNameForCrossEntityAggregateMetrics());
          violations.addAll(
              AbstractMonitoringValidator
                  .<T, RoleMonitoringDefinitionsDescriptor>forViolation(
                      msg,
                      role,
                      role.getNameForCrossEntityAggregateMetrics(),
                      path));
          path = path.removeFromHead();
        } else if (null != role.getNameForCrossEntityAggregateMetrics()) {
          namesForCrossEntityAggregatesSeen.add(
              role.getNameForCrossEntityAggregateMetrics());
        }
      }
    }

    if (null != serviceDescriptor.getMetricEntityTypeDefinitions()) {
      for (MetricEntityTypeDescriptor entity :
           serviceDescriptor.getMetricEntityTypeDefinitions()) {
        if (null != entity.getNameForCrossEntityAggregateMetrics() &&
            namesForCrossEntityAggregatesSeen.contains(
                entity.getNameForCrossEntityAggregateMetrics())) {
          path = path.addBeanNode(entity);
          String msg = String.format(
            "'%s' name for cross entity aggregates is already in use",
            entity.getNameForCrossEntityAggregateMetrics());
          violations.addAll(
              AbstractMonitoringValidator
                  .<T, MetricEntityTypeDescriptor>forViolation(
                msg,
                entity,
                entity.getNameForCrossEntityAggregateMetrics(),
                path));
          path = path.removeFromHead();
        } else if (null != entity.getNameForCrossEntityAggregateMetrics()) {
          namesForCrossEntityAggregatesSeen.add(
              entity.getNameForCrossEntityAggregateMetrics());
        }
      }
    }
    return violations;
  }
} public class NameForCrossEntityAggregatesPrefixedWithServiceNameValidator
    extends AbstractMonitoringValidator<String> {

  private final ImmutableSet<String> builtInNamesForCrossEntityAggregateMetrics;
  private final boolean forServiceNodes;

  public NameForCrossEntityAggregatesPrefixedWithServiceNameValidator(
      ImmutableSet<String> builtInNamesForCrossEntityAggregateMetrics,
      boolean forServiceNodes) {
    Preconditions.checkNotNull(builtInNamesForCrossEntityAggregateMetrics);
    this.builtInNamesForCrossEntityAggregateMetrics =
        builtInNamesForCrossEntityAggregateMetrics;
    this.forServiceNodes = forServiceNodes;
  }

  @Override
  public String getDescription() {
    return
        "Validates that the name for cross entity aggregate metrics starts " +
        "with the service name and an underscore, e.g., 'echo_'.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      String nameForCrossEntityAggregateMetrics,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    if (null == nameForCrossEntityAggregateMetrics) {
      // This is valid, we will construct the name ourselves.
      return noViolations();
    }

    if (builtInNamesForCrossEntityAggregateMetrics.contains(
        nameForCrossEntityAggregateMetrics)) {
      return noViolations();
    }
    String serviceName;
    if (forServiceNodes) {
      // The service prefix in the service name for cross-entity aggregate
      // metrics does not need to end with an underscore, e.g., "echos" for the
      // ECHO service.
      serviceName = context.serviceDescriptor.getName().toLowerCase();
    } else {
      // The service prefix in roles or entities names for cross-entity aggregate
      // metrics must end with an underscore, e.g., "echo_webservers".
      serviceName = context.serviceDescriptor.getName().toLowerCase() + "_";
    }

    if (!nameForCrossEntityAggregateMetrics.startsWith(serviceName)) {
      String msg = String.format(
          "Name for cross entity aggregate metrics '%s' does not start with " +
          "the service name",
          nameForCrossEntityAggregateMetrics);
      return forViolation(msg,
                          nameForCrossEntityAggregateMetrics,
                          nameForCrossEntityAggregateMetrics,
                          path);
    }
    return noViolations();
  }
} public class ParentsAreReachableUsingAttributesValidator extends
    AbstractMonitoringValidator<MetricEntityTypeDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that the names of the parents of a MetricEntityType can be " +
        "constructed using the attributes defined for the entity.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricEntityTypeDescriptor entity,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(path);
    if (null == entity.getParentMetricEntityTypeNames()) {
      return noViolations();
    }
    List<ConstraintViolation<T>> ret = Lists.newArrayList();
    path = constructPathFromProperty(entity,
                                     "parentMetricEntityTypeNames",
                                     path);
    for (String parentName : entity.getParentMetricEntityTypeNames()) {
      MetricEntityTypeDescriptor parent =
          context.entitiesDefined.get(parentName);
      if (null == parent || null == parent.getEntityNameFormat()) {
        // We just ignored this. It will be caught by a different validator.
        continue;
      }
      Set<String> attributes = Sets.newHashSet();
      safeAddAllToCollection(attributes, entity.getImmutableAttributeNames());
      safeAddAllToCollection(attributes, entity.getMutableAttributeNames());
      for (String parentNamePart : parent.getEntityNameFormat()) {
        if (!attributes.contains(parentNamePart)) {
          String msg = String.format(
              "Metric entity type '%s' does not have attribute '%s' to be " +
              "able to construct the name of the parent '%s'",
              entity.getName(),
              parentNamePart,
              parent.getName());
          ret.addAll(
              AbstractMonitoringValidator.<T, MetricEntityTypeDescriptor>
                  forViolation(msg, entity, parent.getName(), path));
        }
      }
    }
    return ret;
  }
} public class WeightingMetricValidator
    extends AbstractMonitoringValidator<MetricDescriptor> {

  @Override
  public String getDescription() {
    return
        "Validates that the weighting metric name refers to a metric defined " +
        "in the MDL.";
  }

  @Override
  public <T> List<ConstraintViolation<T>> validate(
      MonitoringValidationContext context,
      MetricDescriptor metricDescriptor,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metricDescriptor);
    Preconditions.checkNotNull(path);
    path = constructPathFromProperty(metricDescriptor, "name", path);
    String weightingMetric = metricDescriptor.getWeightingMetricName();
    if (null == weightingMetric || weightingMetric.isEmpty()) {
      return noViolations();
    }
    if (!context.metricsDefined.containsKey(weightingMetric)) {
      String msg = String.format(
        "Weighting metric '%s' for metric '%s' refers to unknown metric. ",
        metricDescriptor.getWeightingMetricName(),
        metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, weightingMetric, path);
    }
    if (weightingMetric.equals(metricDescriptor.getName())) {
      String msg = String.format(
        "Weighting metric for metric '%s' refers to itself. ",
        metricDescriptor.getName());
      return forViolation(msg, metricDescriptor, weightingMetric, path);
    }
    return noViolations();
  }
}
import com.cloudera.csd.descriptors.MetricDescriptor;
import com.cloudera.csd.descriptors.MetricEntityAttributeDescriptor;
import com.cloudera.csd.descriptors.MetricEntityTypeDescriptor;
import com.cloudera.csd.descriptors.RoleMonitoringDefinitionsDescriptor;
import com.cloudera.csd.descriptors.ServiceMonitoringDefinitionsDescriptor;
import com.cloudera.csd.validation.monitoring.AbstractMonitoringValidator;
import com.cloudera.csd.validation.monitoring.MonitoringValidationContext;
import com.cloudera.csd.validation.monitoring.constraints.AdditionalAttributesReferToExistingAttributesValidator;
import com.cloudera.csd.validation.monitoring.constraints.AttributeNamePrefixedWithServiceNameValidator;
import com.cloudera.csd.validation.monitoring.constraints.AttributesReferToExistingAttributesValidator;
import com.cloudera.csd.validation.monitoring.constraints.ConsistentMetricDefinitionValidator;
import com.cloudera.csd.validation.monitoring.constraints.CounterMetricNameValidator;
import com.cloudera.csd.validation.monitoring.constraints.DenominatorValidator;
import com.cloudera.csd.validation.monitoring.constraints.EntityNamePrefixedWithServiceNameValidator;
import com.cloudera.csd.validation.monitoring.constraints.EntityParentsReferToExistingEntitiesValidator;
import com.cloudera.csd.validation.monitoring.constraints.MetricNamePrefixedWithServiceNameValidator;
import com.cloudera.csd.validation.monitoring.constraints.NameForCrossEntityAggregatesIsUniqueValidator;
import com.cloudera.csd.validation.monitoring.constraints.NameForCrossEntityAggregatesPrefixedWithServiceNameValidator;
import com.cloudera.csd.validation.monitoring.constraints.ParentsAreReachableUsingAttributesValidator;
import com.cloudera.csd.validation.monitoring.constraints.WeightingMetricValidator;
import com.cloudera.csd.validation.references.ReferenceValidator;
import com.cloudera.csd.validation.references.components.DescriptorPathImpl;
import com.cloudera.validation.DescriptorValidator;
import com.cloudera.validation.DescriptorValidatorImpl;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
                      ServiceMonitoringDefinitionsDescriptor descriptor,
                      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(attribute);
    Preconditions.checkNotNull(descriptor);
    Preconditions.checkNotNull(path);
    Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>> ret =
        Sets.newLinkedHashSet();
    for (AbstractMonitoringValidator<MetricEntityAttributeDescriptor> validator :
         ImmutableList.of(
            new AttributeNamePrefixedWithServiceNameValidator(
                builtInAttributes))) {
      ret.addAll(validator.<ServiceMonitoringDefinitionsDescriptor>validate(
          context,
          attribute,
          path));
    }
    return ret;
  }

  private Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>>
    validateEntity(MonitoringValidationContext context,
                   MetricEntityTypeDescriptor entity,
                   ServiceMonitoringDefinitionsDescriptor descriptor,
                   DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(entity);
    Preconditions.checkNotNull(descriptor);
    Preconditions.checkNotNull(path);
    Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>> ret =
        Sets.newLinkedHashSet();
    // The built in CMSERVER entity violates a number of our rules: it has no
    // name format and no immutable attributes for one. We skip validating it.
    if (entity.getName().equals("CMSERVER")) {
      return ret;
    }
    for (AbstractMonitoringValidator<MetricEntityTypeDescriptor> validator :
         ImmutableList.of(
            new EntityNamePrefixedWithServiceNameValidator(builtInEntityTypes),
            new EntityParentsReferToExistingEntitiesValidator(
                builtInRoleTypes,
                builtInEntityTypes),
            new ParentsAreReachableUsingAttributesValidator(),
            new AttributesReferToExistingAttributesValidator(
                builtInAttributes))) {
      ret.addAll(validator.<ServiceMonitoringDefinitionsDescriptor>validate(
          context,
          entity,
          path));
    }
    return ret;
  }

  private List<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>>
    validateNamesForCrossEntityAggregates(
        MonitoringValidationContext context,
        ServiceMonitoringDefinitionsDescriptor descriptor,
        DescriptorPathImpl root) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(descriptor);
    Preconditions.checkNotNull(root);
    NameForCrossEntityAggregatesIsUniqueValidator validator =
        new NameForCrossEntityAggregatesIsUniqueValidator();
    return validator.validate(context, descriptor, root);
  }

  private Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>>
    validateNameForCrossEntityAggregates(
      MonitoringValidationContext context,
      @Nullable String nameForCrossEntityAggregateMetrics,
      DescriptorPathImpl path,
      boolean serviceNode) {
    Preconditions.checkNotNull(context);
    NameForCrossEntityAggregatesPrefixedWithServiceNameValidator validator =
        new NameForCrossEntityAggregatesPrefixedWithServiceNameValidator(
            builtInNamesForCrossEntityAggregateMetrics,
            serviceNode);
    Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>> ret =
        Sets.newLinkedHashSet();
    ret.addAll(validator.<ServiceMonitoringDefinitionsDescriptor>validate(
        context, nameForCrossEntityAggregateMetrics, path));
    return ret;
  }

  private Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>>
    validateMetrics(
      MonitoringValidationContext context,
      @Nullable List<MetricDescriptor> metricDefinitions,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(path);
    if (null == metricDefinitions) {
      return ImmutableSet.of();
    }

    Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>> ret =
        Sets.newLinkedHashSet();
    for (MetricDescriptor metric : metricDefinitions) {
      ret.addAll(validateMetric(context, metric, path));
    }
    return ret;
  }

  private Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>>
    validateMetric(
      MonitoringValidationContext context,
      MetricDescriptor metric,
      DescriptorPathImpl path) {
    Preconditions.checkNotNull(context);
    Preconditions.checkNotNull(metric);
    Preconditions.checkNotNull(path);
    Set<ConstraintViolation<ServiceMonitoringDefinitionsDescriptor>> ret =
        Sets.newLinkedHashSet();
    path = path.addBeanNode(metric);
    for (AbstractMonitoringValidator<MetricDescriptor> validator :
         ImmutableList.of(
             new MetricNamePrefixedWithServiceNameValidator(),
             new CounterMetricNameValidator(),
             new DenominatorValidator(),
             new WeightingMetricValidator(),
             new ConsistentMetricDefinitionValidator())) {