/* * Copyright 2016 KairosDB Authors * * 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 org.kairosdb.core.http.rest.json; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import com.google.common.collect.TreeMultimap; import com.google.gson.*; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.google.inject.Inject; import org.apache.bval.constraints.NotEmpty; import org.apache.bval.jsr303.ApacheValidationProvider; import org.joda.time.DateTimeZone; import org.kairosdb.core.aggregator.*; import org.kairosdb.core.datastore.*; import org.kairosdb.core.groupby.GroupBy; import org.kairosdb.core.groupby.GroupByFactory; import org.kairosdb.core.http.rest.BeanValidationException; import org.kairosdb.core.http.rest.QueryException; import org.kairosdb.rollup.Rollup; import org.kairosdb.rollup.RollupTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.validation.*; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.metadata.ConstraintDescriptor; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.*; public class QueryParser { private static final Logger logger = LoggerFactory.getLogger(QueryParser.class); private static final Validator VALIDATOR = Validation.byProvider(ApacheValidationProvider.class).configure().buildValidatorFactory().getValidator(); private AggregatorFactory m_aggregatorFactory; private QueryPluginFactory m_pluginFactory; private GroupByFactory m_groupByFactory; private Map<Class, Map<String, PropertyDescriptor>> m_descriptorMap; private final Object m_descriptorMapLock = new Object(); private Gson m_gson; @Inject public QueryParser(AggregatorFactory aggregatorFactory, GroupByFactory groupByFactory, QueryPluginFactory pluginFactory) { m_aggregatorFactory = aggregatorFactory; m_groupByFactory = groupByFactory; m_pluginFactory = pluginFactory; m_descriptorMap = new HashMap<>(); GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory()); builder.registerTypeAdapter(TimeUnit.class, new TimeUnitDeserializer()); builder.registerTypeAdapter(DateTimeZone.class, new DateTimeZoneDeserializer()); builder.registerTypeAdapter(Metric.class, new MetricDeserializer()); builder.registerTypeAdapter(SetMultimap.class, new SetMultimapDeserializer()); builder.registerTypeAdapter(RelativeTime.class, new RelativeTimeSerializer()); builder.registerTypeAdapter(SetMultimap.class, new SetMultimapSerializer()); builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); m_gson = builder.create(); } public Gson getGson() { return m_gson; } private PropertyDescriptor getPropertyDescriptor(Class objClass, String property) throws IntrospectionException { synchronized (m_descriptorMapLock) { Map<String, PropertyDescriptor> propMap = m_descriptorMap.get(objClass); if (propMap == null) { propMap = new HashMap<>(); m_descriptorMap.put(objClass, propMap); BeanInfo beanInfo = Introspector.getBeanInfo(objClass); PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { propMap.put(getUnderscorePropertyName(descriptor.getName()), descriptor); } } return (propMap.get(property)); } } public static String getUnderscorePropertyName(String camelCaseName) { StringBuilder sb = new StringBuilder(); for (char c : camelCaseName.toCharArray()) { if (Character.isUpperCase(c)) sb.append('_').append(Character.toLowerCase(c)); else sb.append(c); } return (sb.toString()); } private void validateObject(Object object) throws BeanValidationException { validateObject(object, null); } private void validateObject(Object object, String context) throws BeanValidationException { // validate object using the bean validation framework Set<ConstraintViolation<Object>> violations = VALIDATOR.validate(object); if (!violations.isEmpty()) { throw new BeanValidationException(violations, context); } } public List<QueryMetric> parseQueryMetric(String json) throws QueryException, BeanValidationException { JsonParser parser = new JsonParser(); JsonObject obj = parser.parse(json).getAsJsonObject(); return parseQueryMetric(obj); } private List<QueryMetric> parseQueryMetric(JsonObject obj) throws QueryException, BeanValidationException { return parseQueryMetric(obj, ""); } private List<QueryMetric> parseQueryMetric(JsonObject obj, String contextPrefix) throws QueryException, BeanValidationException { List<QueryMetric> ret = new ArrayList<>(); Query query; try { query = m_gson.fromJson(obj, Query.class); validateObject(query); } catch (ContextualJsonSyntaxException e) { throw new BeanValidationException(new SimpleConstraintViolation(e.getContext(), e.getMessage()), "query"); } JsonArray metricsArray = obj.getAsJsonArray("metrics"); if (metricsArray == null) { throw new BeanValidationException(new SimpleConstraintViolation("metric[]", "must have a size of at least 1"), contextPrefix + "query"); } for (int I = 0; I < metricsArray.size(); I++) { String context = (!contextPrefix.isEmpty() ? contextPrefix + "." : contextPrefix) + "query.metric[" + I + "]"; try { Metric metric = m_gson.fromJson(metricsArray.get(I), Metric.class); validateObject(metric, context); long startTime = getStartTime(query, context); QueryMetric queryMetric = new QueryMetric(startTime, query.getCacheTime(), metric.getName()); queryMetric.setExcludeTags(metric.isExcludeTags()); queryMetric.setLimit(metric.getLimit()); long endTime = getEndTime(query); if (endTime > -1) queryMetric.setEndTime(endTime); if (queryMetric.getEndTime() < startTime) throw new BeanValidationException(new SimpleConstraintViolation("end_time", "must be greater than the start time"), context); queryMetric.setCacheString(query.getCacheString() + metric.getCacheString()); JsonObject jsMetric = metricsArray.get(I).getAsJsonObject(); JsonElement group_by = jsMetric.get("group_by"); if (group_by != null) { JsonArray groupBys = group_by.getAsJsonArray(); parseGroupBy(context, queryMetric, groupBys); } JsonElement aggregators = jsMetric.get("aggregators"); if (aggregators != null) { JsonArray asJsonArray = aggregators.getAsJsonArray(); if (asJsonArray.size() > 0) parseAggregators(context, queryMetric, asJsonArray, query.getTimeZone()); } JsonElement plugins = jsMetric.get("plugins"); if (plugins != null) { JsonArray pluginArray = plugins.getAsJsonArray(); if (pluginArray.size() > 0) parsePlugins(context, queryMetric, pluginArray); } JsonElement order = jsMetric.get("order"); if (order != null) queryMetric.setOrder(Order.fromString(order.getAsString(), context)); queryMetric.setTags(metric.getTags()); ret.add(queryMetric); } catch (ContextualJsonSyntaxException e) { throw new BeanValidationException(new SimpleConstraintViolation(e.getContext(), e.getMessage()), context); } } return (ret); } public List<RollupTask> parseRollupTasks(String json) throws BeanValidationException, QueryException { List<RollupTask> tasks = new ArrayList<>(); JsonParser parser = new JsonParser(); JsonArray rollupTasks = parser.parse(json).getAsJsonArray(); for (int i = 0; i < rollupTasks.size(); i++) { JsonObject taskObject = rollupTasks.get(i).getAsJsonObject(); RollupTask task = parseRollupTask(taskObject, "tasks[" + i + "]"); task.addJson(taskObject.toString().replaceAll("\\n", "")); tasks.add(task); } return tasks; } public RollupTask parseRollupTask(String json) throws BeanValidationException, QueryException { JsonParser parser = new JsonParser(); JsonObject taskObject = parser.parse(json).getAsJsonObject(); RollupTask task = parseRollupTask(taskObject, ""); task.addJson(taskObject.toString().replaceAll("\\n", "")); return task; } public RollupTask parseRollupTask(JsonObject rollupTask, String context) throws BeanValidationException, QueryException { RollupTask task = m_gson.fromJson(rollupTask.getAsJsonObject(), RollupTask.class); validateObject(task); JsonArray rollups = rollupTask.getAsJsonObject().getAsJsonArray("rollups"); if (rollups != null) { for (int j = 0; j < rollups.size(); j++) { JsonObject rollupObject = rollups.get(j).getAsJsonObject(); Rollup rollup = m_gson.fromJson(rollupObject, Rollup.class); context = context + "rollup[" + j + "]"; validateObject(rollup, context); JsonObject queryObject = rollupObject.getAsJsonObject("query"); List<QueryMetric> queries = parseQueryMetric(queryObject, context); for (int k = 0; k < queries.size(); k++) { QueryMetric query = queries.get(k); context += ".query[" + k + "]"; validateHasRangeAggregator(query, context); // Add aggregators needed for rollups SaveAsAggregator saveAsAggregator = (SaveAsAggregator) m_aggregatorFactory.createAggregator("save_as"); saveAsAggregator.setMetricName(rollup.getSaveAs()); TrimAggregator trimAggregator = (TrimAggregator) m_aggregatorFactory.createAggregator("trim"); trimAggregator.setTrim(TrimAggregator.Trim.LAST); query.addAggregator(saveAsAggregator); query.addAggregator(trimAggregator); } rollup.addQueries(queries); task.addRollup(rollup); } } return task; } private void validateHasRangeAggregator(QueryMetric query, String context) throws BeanValidationException { boolean hasRangeAggregator = false; for (Aggregator aggregator : query.getAggregators()) { if (aggregator instanceof RangeAggregator) { hasRangeAggregator = true; break; } } if (!hasRangeAggregator) { throw new BeanValidationException(new SimpleConstraintViolation("aggregator", "At least one aggregator must be a range aggregator"), context); } } private void parsePlugins(String context, QueryMetric queryMetric, JsonArray plugins) throws BeanValidationException, QueryException { for (int I = 0; I < plugins.size(); I++) { JsonObject pluginJson = plugins.get(I).getAsJsonObject(); JsonElement name = pluginJson.get("name"); if (name == null || name.getAsString().isEmpty()) throw new BeanValidationException(new SimpleConstraintViolation("plugins[" + I + "]", "must have a name"), context); String pluginContext = context + ".plugins[" + I + "]"; String pluginName = name.getAsString(); QueryPlugin plugin = m_pluginFactory.createQueryPlugin(pluginName); if (plugin == null) throw new BeanValidationException(new SimpleConstraintViolation(pluginName, "invalid query plugin name"), pluginContext); deserializeProperties(pluginContext, pluginJson, pluginName, plugin); validateObject(plugin, pluginContext); queryMetric.addPlugin(plugin); } } private void parseAggregators(String context, QueryMetric queryMetric, JsonArray aggregators, DateTimeZone timeZone) throws QueryException, BeanValidationException { for (int J = 0; J < aggregators.size(); J++) { JsonObject jsAggregator = aggregators.get(J).getAsJsonObject(); JsonElement name = jsAggregator.get("name"); if (name == null || name.getAsString().isEmpty()) throw new BeanValidationException(new SimpleConstraintViolation("aggregators[" + J + "]", "must have a name"), context); String aggContext = context + ".aggregators[" + J + "]"; String aggName = name.getAsString(); Aggregator aggregator = m_aggregatorFactory.createAggregator(aggName); if (aggregator == null) throw new BeanValidationException(new SimpleConstraintViolation(aggName, "invalid aggregator name"), aggContext); //If it is a range aggregator we will default the start time to //the start of the query. if (aggregator instanceof RangeAggregator) { RangeAggregator ra = (RangeAggregator) aggregator; ra.setStartTime(queryMetric.getStartTime()); } if (aggregator instanceof TimezoneAware) { TimezoneAware ta = (TimezoneAware) aggregator; ta.setTimeZone(timeZone); } if (aggregator instanceof GroupByAware) { GroupByAware groupByAware = (GroupByAware) aggregator; groupByAware.setGroupBys(queryMetric.getGroupBys()); } deserializeProperties(aggContext, jsAggregator, aggName, aggregator); validateObject(aggregator, aggContext); queryMetric.addAggregator(aggregator); } } private void parseGroupBy(String context, QueryMetric queryMetric, JsonArray groupBys) throws QueryException, BeanValidationException { for (int J = 0; J < groupBys.size(); J++) { String groupContext = "group_by[" + J + "]"; JsonObject jsGroupBy = groupBys.get(J).getAsJsonObject(); JsonElement nameElement = jsGroupBy.get("name"); if (nameElement == null || nameElement.getAsString().isEmpty()) throw new BeanValidationException(new SimpleConstraintViolation(groupContext, "must have a name"), context); String name = nameElement.getAsString(); GroupBy groupBy = m_groupByFactory.createGroupBy(name); if (groupBy == null) throw new BeanValidationException(new SimpleConstraintViolation(groupContext + "." + name, "invalid group_by name"), context); deserializeProperties(context + "." + groupContext, jsGroupBy, name, groupBy); validateObject(groupBy, context + "." + groupContext); groupBy.setStartDate(queryMetric.getStartTime()); queryMetric.addGroupBy(groupBy); } } private void deserializeProperties(String context, JsonObject jsonObject, String name, Object object) throws QueryException, BeanValidationException { Set<Map.Entry<String, JsonElement>> props = jsonObject.entrySet(); for (Map.Entry<String, JsonElement> prop : props) { String property = prop.getKey(); if (property.equals("name")) continue; PropertyDescriptor pd = null; try { pd = getPropertyDescriptor(object.getClass(), property); } catch (IntrospectionException e) { logger.error("Introspection error on " + object.getClass(), e); } if (pd == null) { String msg = "Property '" + property + "' was specified for object '" + name + "' but no matching setter was found on '" + object.getClass() + "'"; throw new QueryException(msg); } Class<?> propClass = pd.getPropertyType(); Object propValue; try { propValue = m_gson.fromJson(prop.getValue(), propClass); validateObject(propValue, context + "." + property); } catch (ContextualJsonSyntaxException e) { throw new BeanValidationException(new SimpleConstraintViolation(e.getContext(), e.getMessage()), context); } catch (NumberFormatException e) { throw new BeanValidationException(new SimpleConstraintViolation(property, e.getMessage()), context); } Method method = pd.getWriteMethod(); if (method == null) { String msg = "Property '" + property + "' was specified for object '" + name + "' but no matching setter was found on '" + object.getClass().getName() + "'"; throw new QueryException(msg); } try { method.invoke(object, propValue); } catch (Exception e) { logger.error("Invocation error: ", e); String msg = "Call to " + object.getClass().getName() + ":" + method.getName() + " failed with message: " + e.getMessage(); throw new QueryException(msg); } } } private long getStartTime(Query request, String context) throws BeanValidationException { if (request.getStartAbsolute() != null) { return request.getStartAbsolute(); } else if (request.getStartRelative() != null) { return request.getStartRelative().getTimeRelativeTo(System.currentTimeMillis()); } else { throw new BeanValidationException(new SimpleConstraintViolation("start_time", "relative or absolute time must be set"), context); } } private long getEndTime(Query request) { if (request.getEndAbsolute() != null) return request.getEndAbsolute(); else if (request.getEndRelative() != null) return request.getEndRelative().getTimeRelativeTo(System.currentTimeMillis()); return -1; } //=========================================================================== private static class Metric { @NotNull @NotEmpty() @SerializedName("name") private String name; @SerializedName("tags") private SetMultimap<String, String> tags; @SerializedName("exclude_tags") private boolean exclude_tags; @SerializedName("limit") private int limit; public Metric(String name, boolean exclude_tags, TreeMultimap<String, String> tags) { this.name = name; this.tags = tags; this.exclude_tags = exclude_tags; this.limit = 0; } public String getName() { return name; } public int getLimit() { return limit; } public void setLimit(int limit) { this.limit = limit; } private boolean isExcludeTags() { return exclude_tags; } public String getCacheString() { StringBuilder sb = new StringBuilder(); sb.append(name).append(":"); for (Map.Entry<String, String> tagEntry : tags.entries()) { sb.append(tagEntry.getKey()).append("="); sb.append(tagEntry.getValue()).append(":"); } return (sb.toString()); } public SetMultimap<String, String> getTags() { if (tags != null) { return tags; } else { return HashMultimap.create(); } } } //=========================================================================== private static class Query { @SerializedName("start_absolute") private Long m_startAbsolute; @SerializedName("end_absolute") private Long m_endAbsolute; @Min(0) @SerializedName("cache_time") private int cache_time; @Valid @SerializedName("start_relative") private RelativeTime start_relative; @Valid @SerializedName("end_relative") private RelativeTime end_relative; @Valid @SerializedName("time_zone") private DateTimeZone m_timeZone;// = DateTimeZone.UTC;; public Long getStartAbsolute() { return m_startAbsolute; } public Long getEndAbsolute() { return m_endAbsolute; } public int getCacheTime() { return cache_time; } public RelativeTime getStartRelative() { return start_relative; } public RelativeTime getEndRelative() { return end_relative; } public DateTimeZone getTimeZone() { return m_timeZone; } public String getCacheString() { StringBuilder sb = new StringBuilder(); if (m_startAbsolute != null) sb.append(m_startAbsolute).append(":"); if (start_relative != null) sb.append(start_relative.toString()).append(":"); if (m_endAbsolute != null) sb.append(m_endAbsolute).append(":"); if (end_relative != null) sb.append(end_relative.toString()).append(":"); return (sb.toString()); } @Override public String toString() { return "Query{" + "startAbsolute='" + m_startAbsolute + '\'' + ", endAbsolute='" + m_endAbsolute + '\'' + ", cache_time=" + cache_time + ", startRelative=" + start_relative + ", endRelative=" + end_relative + '}'; } } //=========================================================================== private static class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory { public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { @SuppressWarnings("unchecked") Class<T> rawType = (Class<T>) type.getRawType(); if (!rawType.isEnum()) { return null; } final Map<String, T> lowercaseToConstant = new HashMap<>(); for (T constant : rawType.getEnumConstants()) { lowercaseToConstant.put(toLowercase(constant), constant); } return new TypeAdapter<T>() { public void write(JsonWriter out, T value) throws IOException { if (value == null) { out.nullValue(); } else { out.value(toLowercase(value)); } } public T read(JsonReader reader) throws IOException { if (reader.peek() == JsonToken.NULL) { reader.nextNull(); return null; } else { return lowercaseToConstant.get(reader.nextString()); } } }; } private String toLowercase(Object o) { return o.toString().toLowerCase(Locale.US); } } //=========================================================================== private static class TimeUnitDeserializer implements JsonDeserializer<TimeUnit> { public TimeUnit deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { String unit = json.getAsString(); TimeUnit tu; try { tu = TimeUnit.from(unit); } catch (IllegalArgumentException e) { throw new ContextualJsonSyntaxException(unit, "is not a valid time unit, must be one of " + TimeUnit.toValueNames()); } return tu; } } //=========================================================================== private static class DateTimeZoneDeserializer implements JsonDeserializer<DateTimeZone> { public DateTimeZone deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (json.isJsonNull()) return null; String tz = json.getAsString(); if (tz.isEmpty()) // defaults to UTC return DateTimeZone.UTC; DateTimeZone timeZone; try { // check if time zone is valid timeZone = DateTimeZone.forID(tz); } catch (IllegalArgumentException e) { throw new ContextualJsonSyntaxException(tz, "is not a valid time zone, must be one of " + DateTimeZone.getAvailableIDs()); } return timeZone; } } //=========================================================================== private static class MetricDeserializer implements JsonDeserializer<Metric> { @Override public Metric deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { JsonObject jsonObject = jsonElement.getAsJsonObject(); String name = null; if (jsonObject.get("name") != null) name = jsonObject.get("name").getAsString(); boolean exclude_tags = false; if (jsonObject.get("exclude_tags") != null) exclude_tags = jsonObject.get("exclude_tags").getAsBoolean(); TreeMultimap<String, String> tags = TreeMultimap.create(); JsonElement jeTags = jsonObject.get("tags"); if (jeTags != null) { JsonObject joTags = jeTags.getAsJsonObject(); int count = 0; for (Map.Entry<String, JsonElement> tagEntry : joTags.entrySet()) { String context = "tags[" + count + "]"; if (tagEntry.getKey().isEmpty()) throw new ContextualJsonSyntaxException(context, "name must not be empty"); if (tagEntry.getValue().isJsonArray()) { for (JsonElement element : tagEntry.getValue().getAsJsonArray()) { if (element.isJsonNull() || element.getAsString().isEmpty()) throw new ContextualJsonSyntaxException(context + "." + tagEntry.getKey(), "value must not be null or empty"); tags.put(tagEntry.getKey(), element.getAsString()); } } else { if (tagEntry.getValue().isJsonNull() || tagEntry.getValue().getAsString().isEmpty()) throw new ContextualJsonSyntaxException(context + "." + tagEntry.getKey(), "value must not be null or empty"); tags.put(tagEntry.getKey(), tagEntry.getValue().getAsString()); } count++; } } Metric ret = new Metric(name, exclude_tags, tags); JsonElement limit = jsonObject.get("limit"); if (limit != null) ret.setLimit(limit.getAsInt()); return (ret); } } //=========================================================================== private static class ContextualJsonSyntaxException extends RuntimeException { private String context; private ContextualJsonSyntaxException(String context, String msg) { super(msg); this.context = context; } private String getContext() { return context; } } //=========================================================================== public static class SimpleConstraintViolation implements ConstraintViolation<Object> { private String message; private String context; public SimpleConstraintViolation(String context, String message) { this.message = message; this.context = context; } @Override public String getMessage() { return message; } @Override public String getMessageTemplate() { return null; } @Override public Object getRootBean() { return null; } @Override public Class<Object> getRootBeanClass() { return null; } @Override public Object getLeafBean() { return null; } @Override public Path getPropertyPath() { return new SimplePath(context); } @Override public Object getInvalidValue() { return null; } @Override public ConstraintDescriptor<?> getConstraintDescriptor() { return null; } } private static class SimplePath implements Path { private String context; private SimplePath(String context) { this.context = context; } @Override public Iterator<Node> iterator() { return null; } @Override public String toString() { return context; } } }