/*
* Copyright 2014-2017 Netflix, 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.netflix.spectator.atlas.impl;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Tag;
import com.netflix.spectator.impl.Preconditions;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Query for matching based on tags. For more information see:
*
* https://github.com/Netflix/atlas/wiki/Stack-Language#query
*
* <b>Classes in this package are only intended for use internally within spectator. They may
* change at any time and without notice.</b>
*/
public interface Query {
/** Convert {@code id} to a map. */
static Map<String, String> toMap(Id id) {
Map<String, String> tags = new HashMap<>();
for (Tag t : id.tags()) {
tags.put(t.key(), t.value());
}
tags.put("name", id.name());
return tags;
}
/**
* Check to see if this query matches a set of tags. Common tags or changes to fix
* invalid characters should be performed prior to checking for a match.
*
* @param tags
* Tags to use when checking for a match.
* @return
* True if the query expression matches the tag map.
*/
boolean matches(Map<String, String> tags);
/**
* Check to see if this query matches an id. Equivalent to calling {@link #matches(Map)}
* with the result of {@link #toMap(Id)}.
*
* @param id
* Id to use when checking for a match.
* @return
* True if the query expression matches the id.
*/
default boolean matches(Id id) {
return matches(toMap(id));
}
/**
* Extract the tags from the query that have an exact match for a given value. That
* is are specified using an {@link Equal} clause.
*
* @return
* Tags that are exactly matched as part of the query.
*/
default Map<String, String> exactTags() {
return Collections.emptyMap();
}
/** Returns a new query: {@code this AND q}. */
default Query and(Query q) {
return new And(this, q);
}
/** Returns a new query: {@code this OR q}. */
default Query or(Query q) {
return new Or(this, q);
}
/** Returns an inverted version of this query. */
default Query not() {
return new Not(this);
}
/** Query that always matches. */
Query TRUE = new Query() {
@Override public boolean matches(Map<String, String> tags) {
return true;
}
@Override public String toString() {
return ":true";
}
};
/** Query that never matches. */
Query FALSE = new Query() {
@Override public boolean matches(Map<String, String> tags) {
return false;
}
@Override public String toString() {
return ":false";
}
};
/** Query that matches if both sub-queries match. */
final class And implements Query {
private final Query q1;
private final Query q2;
/** Create a new instance. */
And(Query q1, Query q2) {
this.q1 = Preconditions.checkNotNull(q1, "q1");
this.q2 = Preconditions.checkNotNull(q2, "q2");
}
@Override public boolean matches(Map<String, String> tags) {
return q1.matches(tags) && q2.matches(tags);
}
@Override public Map<String, String> exactTags() {
Map<String, String> tags = new HashMap<>();
tags.putAll(q1.exactTags());
tags.putAll(q2.exactTags());
return tags;
}
@Override public String toString() {
return q1 + "," + q2 + ",:and";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof And)) return false;
And other = (And) obj;
return q1.equals(other.q1) && q2.equals(other.q2);
}
@Override public int hashCode() {
int result = q1.hashCode();
result = 31 * result + q2.hashCode();
return result;
}
}
/** Query that matches if either sub-queries match. */
final class Or implements Query {
private final Query q1;
private final Query q2;
/** Create a new instance. */
Or(Query q1, Query q2) {
this.q1 = Preconditions.checkNotNull(q1, "q1");
this.q2 = Preconditions.checkNotNull(q2, "q2");
}
@Override public boolean matches(Map<String, String> tags) {
return q1.matches(tags) || q2.matches(tags);
}
@Override public String toString() {
return q1 + "," + q2 + ",:or";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof Or)) return false;
Or other = (Or) obj;
return q1.equals(other.q1) && q2.equals(other.q2);
}
@Override public int hashCode() {
int result = q1.hashCode();
result = 31 * result + q2.hashCode();
return result;
}
}
/** Query that matches if the sub-query does not match. */
final class Not implements Query {
private final Query q;
/** Create a new instance. */
Not(Query q) {
this.q = Preconditions.checkNotNull(q, "q");
}
@Override public boolean matches(Map<String, String> tags) {
return !q.matches(tags);
}
@Override public String toString() {
return q + ",:not";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof Not)) return false;
Not other = (Not) obj;
return q.equals(other.q);
}
@Override public int hashCode() {
return q.hashCode();
}
}
/** Query that matches if the tag map contains a specified key. */
final class Has implements Query {
private final String k;
/** Create a new instance. */
Has(String k) {
this.k = Preconditions.checkNotNull(k, "k");
}
@Override public boolean matches(Map<String, String> tags) {
return tags.containsKey(k);
}
@Override public String toString() {
return k + ",:has";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof Has)) return false;
Has other = (Has) obj;
return k.equals(other.k);
}
@Override public int hashCode() {
return k.hashCode();
}
}
/** Query that matches if the tag map contains key {@code k} with value {@code v}. */
final class Equal implements Query {
private final String k;
private final String v;
/** Create a new instance. */
Equal(String k, String v) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
}
@Override public boolean matches(Map<String, String> tags) {
return v.equals(tags.get(k));
}
@Override public Map<String, String> exactTags() {
Map<String, String> tags = new HashMap<>();
tags.put(k, v);
return tags;
}
@Override public String toString() {
return k + "," + v + ",:eq";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof Equal)) return false;
Equal other = (Equal) obj;
return k.equals(other.k) && v.equals(other.v);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value in the set
* {@code vs}.
*/
final class In implements Query {
private final String k;
private final Set<String> vs;
/** Create a new instance. */
In(String k, Set<String> vs) {
Preconditions.checkArg(!vs.isEmpty(), "list of values for :in cannot be empty");
this.k = Preconditions.checkNotNull(k, "k");
this.vs = Preconditions.checkNotNull(vs, "vs");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && vs.contains(tags.get(k));
}
@Override public String toString() {
String values = vs.stream().collect(Collectors.joining(","));
return k + ",(," + values + ",),:in";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof In)) return false;
In other = (In) obj;
return k.equals(other.k) && vs.equals(other.vs);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + vs.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value that is lexically
* less than {@code v}.
*/
final class LessThan implements Query {
private final String k;
private final String v;
/** Create a new instance. */
LessThan(String k, String v) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && s.compareTo(v) < 0;
}
@Override public String toString() {
return k + "," + v + ",:lt";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof LessThan)) return false;
LessThan other = (LessThan) obj;
return k.equals(other.k) && v.equals(other.v);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value that is lexically
* less than or equal to {@code v}.
*/
final class LessThanEqual implements Query {
private final String k;
private final String v;
/** Create a new instance. */
LessThanEqual(String k, String v) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && s.compareTo(v) <= 0;
}
@Override public String toString() {
return k + "," + v + ",:le";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof LessThanEqual)) return false;
LessThanEqual other = (LessThanEqual) obj;
return k.equals(other.k) && v.equals(other.v);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value that is lexically
* greater than {@code v}.
*/
final class GreaterThan implements Query {
private final String k;
private final String v;
/** Create a new instance. */
GreaterThan(String k, String v) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && s.compareTo(v) > 0;
}
@Override public String toString() {
return k + "," + v + ",:gt";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof GreaterThan)) return false;
GreaterThan other = (GreaterThan) obj;
return k.equals(other.k) && v.equals(other.v);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value that is lexically
* greater than or equal to {@code v}.
*/
final class GreaterThanEqual implements Query {
private final String k;
private final String v;
/** Create a new instance. */
GreaterThanEqual(String k, String v) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && s.compareTo(v) >= 0;
}
@Override public String toString() {
return k + "," + v + ",:ge";
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof GreaterThanEqual)) return false;
GreaterThanEqual other = (GreaterThanEqual) obj;
return k.equals(other.k) && v.equals(other.v);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
return result;
}
}
/**
* Query that matches if the tag map contains key {@code k} with a value that matches the
* regex in {@code v}. The expression will be automatically anchored to the start to encourage
* prefix matches.
*
* <p><b>Warning:</b> regular expressions are often expensive and can add a lot of overhead.
* Use them sparingly.</p>
*/
final class Regex implements Query {
private final String k;
private final String v;
private final Pattern pattern;
private final String name;
/** Create a new instance. */
Regex(String k, String v) {
this(k, v, 0, ":re");
}
/** Create a new instance. */
Regex(String k, String v, int flags, String name) {
this.k = Preconditions.checkNotNull(k, "k");
this.v = Preconditions.checkNotNull(v, "v");
this.pattern = Pattern.compile("^" + v, flags);
this.name = Preconditions.checkNotNull(name, "name");
}
@Override public boolean matches(Map<String, String> tags) {
String s = tags.get(k);
return s != null && pattern.matcher(s).find();
}
@Override public String toString() {
return k + "," + v + "," + name;
}
@Override public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof Regex)) return false;
Regex other = (Regex) obj;
return k.equals(other.k)
&& v.equals(other.v)
&& pattern.flags() == other.pattern.flags()
&& name.equals(other.name);
}
@Override public int hashCode() {
int result = k.hashCode();
result = 31 * result + v.hashCode();
result = 31 * result + pattern.flags();
result = 31 * result + name.hashCode();
return result;
}
}
}