org.kairosdb.core.http.rest.json.QueryParser.java Source code

Java tutorial

Introduction

Here is the source code for org.kairosdb.core.http.rest.json.QueryParser.java

Source

/*
 * Copyright 2013 Proofpoint 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 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<Class, Map<String, PropertyDescriptor>>();

        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<String, PropertyDescriptor>();
                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<QueryMetric>();

        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<RollupTask>();
        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<String, T>();
            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 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 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 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;
        }
    }

}