net.opentsdb.tree.TreeRule.java Source code

Java tutorial

Introduction

Here is the source code for net.opentsdb.tree.TreeRule.java

Source

// This file is part of OpenTSDB.
// Copyright (C) 2013  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tree;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.hbase.async.Bytes;
import org.hbase.async.DeleteRequest;
import org.hbase.async.GetRequest;
import org.hbase.async.HBaseException;
import org.hbase.async.KeyValue;
import org.hbase.async.PutRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.opentsdb.core.TSDB;
import net.opentsdb.utils.JSON;
import net.opentsdb.utils.JSONException;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;

/**
 * Represents single rule in a set of rules for a given tree. Each rule is
 * uniquely identified by:
 * <ul><li>tree_id - The ID of the tree to which the rule belongs</li>
 * <li>level - Outer processing order where the rule resides. Lower values are
 * processed first. Starts at 0.</li>
 * <li>order - Inner processing order within a given level. Lower values are
 * processed first. Starts at 0.</li></ul>
 * Each rule is stored as an individual column so that they can be modified
 * individually. RPC calls can also bulk replace rule sets.
 * @since 2.0
 */
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY)
public final class TreeRule {

    /** Types of tree rules */
    public enum TreeRuleType {
        METRIC,
        /** A simple metric rule */
        METRIC_CUSTOM,
        /** Matches on UID Meta custom field */
        TAGK,
        /** Matches on a tagk name */
        TAGK_CUSTOM,
        /** Matches on a UID Meta custom field */
        TAGV_CUSTOM /** Matches on a UID Meta custom field */
    }

    private static final Logger LOG = LoggerFactory.getLogger(TreeRule.class);
    /** Charset used to convert Strings to byte arrays and back. */
    private static final Charset CHARSET = Charset.forName("ISO-8859-1");
    /** ASCII Rule prefix. Qualifier is tree_rule:<level>:<order> */
    private static final byte[] RULE_PREFIX = "tree_rule:".getBytes(CHARSET);

    /** Type of rule */
    @JsonDeserialize(using = JSON.TreeRuleTypeDeserializer.class)
    private TreeRuleType type = null;

    /** Name of the field to match on if applicable */
    private String field = "";

    /** Name of the custom field to match on, the key */
    private String custom_field = "";

    /** User supplied regular expression before parsing */
    private String regex = "";

    /** Separation character or string */
    private String separator = "";

    /** An optional description of the rule */
    private String description = "";

    /** Optional notes about the rule */
    private String notes = "";

    /** Optional group index for extracting from regex matches */
    private int regex_group_idx = 0;

    /** Optioanl display format override */
    private String display_format = "";

    /** Required level where the rule resides */
    private int level = 0;

    /** Required order where the rule resides */
    private int order = 0;

    /** The tree this rule belongs to */
    private int tree_id = 0;

    /** Compiled regex pattern, compiled after processing */
    private Pattern compiled_regex = null;

    /** Tracks fields that have changed by the user to avoid overwrites */
    private final HashMap<String, Boolean> changed = new HashMap<String, Boolean>();

    /**
     * Default constructor necessary for de/serialization
     */
    public TreeRule() {
        initializeChangedMap();
    }

    /**
     * Constructor initializes the tree ID
     * @param tree_id The tree this rule belongs to
     */
    public TreeRule(final int tree_id) {
        this.tree_id = tree_id;
        initializeChangedMap();
    }

    /**
     * Copy constructor that creates a completely independent copy of the original
     * object
     * @param original The original object to copy from
     * @throws PatternSyntaxException if the regex is invalid
     */
    public TreeRule(final TreeRule original) {
        custom_field = original.custom_field;
        description = original.description;
        display_format = original.display_format;
        field = original.field;
        level = original.level;
        notes = original.notes;
        order = original.order;
        regex_group_idx = original.regex_group_idx;
        separator = original.separator;
        tree_id = original.tree_id;
        type = original.type;
        setRegex(original.regex);
        initializeChangedMap();
    }

    /**
     * Copies changed fields from the incoming rule to the local rule
     * @param rule The rule to copy from
     * @param overwrite Whether or not to replace all fields in the local object
     * @return True if there were changes, false if everything was identical
     */
    public boolean copyChanges(final TreeRule rule, final boolean overwrite) {
        if (rule == null) {
            throw new IllegalArgumentException("Cannot copy a null rule");
        }
        if (tree_id != rule.tree_id) {
            throw new IllegalArgumentException("Tree IDs do not match");
        }
        if (level != rule.level) {
            throw new IllegalArgumentException("Levels do not match");
        }
        if (order != rule.order) {
            throw new IllegalArgumentException("Orders do not match");
        }

        if (overwrite || (rule.changed.get("type") && type != rule.type)) {
            type = rule.type;
            changed.put("type", true);
        }
        if (overwrite || (rule.changed.get("field") && !field.equals(rule.field))) {
            field = rule.field;
            changed.put("field", true);
        }
        if (overwrite || (rule.changed.get("custom_field") && !custom_field.equals(rule.custom_field))) {
            custom_field = rule.custom_field;
            changed.put("custom_field", true);
        }
        if (overwrite || (rule.changed.get("regex") && !regex.equals(rule.regex))) {
            // validate and compile via the setter
            setRegex(rule.regex);
        }
        if (overwrite || (rule.changed.get("separator") && !separator.equals(rule.separator))) {
            separator = rule.separator;
            changed.put("separator", true);
        }
        if (overwrite || (rule.changed.get("description") && !description.equals(rule.description))) {
            description = rule.description;
            changed.put("description", true);
        }
        if (overwrite || (rule.changed.get("notes") && !notes.equals(rule.notes))) {
            notes = rule.notes;
            changed.put("notes", true);
        }
        if (overwrite || (rule.changed.get("regex_group_idx") && regex_group_idx != rule.regex_group_idx)) {
            regex_group_idx = rule.regex_group_idx;
            changed.put("regex_group_idx", true);
        }
        if (overwrite || (rule.changed.get("display_format") && !display_format.equals(rule.display_format))) {
            display_format = rule.display_format;
            changed.put("display_format", true);
        }
        for (boolean has_changes : changed.values()) {
            if (has_changes) {
                return true;
            }
        }
        return false;
    }

    /** @return the rule ID as [tree_id:level:order] */
    @Override
    public String toString() {
        return "[" + tree_id + ":" + level + ":" + order + ":" + type + "]";
    }

    /**
     * Attempts to write the rule to storage via CompareAndSet, merging changes
     * with an existing rule.
     * <b>Note:</b> If the local object didn't have any fields set by the caller
     * or there weren't any changes, then the data will not be written and an 
     * exception will be thrown.
     * <b>Note:</b> This method also validates the rule, making sure that proper
     * combinations of data exist before writing to storage.
     * @param tsdb The TSDB to use for storage access
     * @param overwrite When the RPC method is PUT, will overwrite all user
     * accessible fields
     * @return True if the CAS call succeeded, false if the stored data was
     * modified in flight. This should be retried if that happens.
     * @throws HBaseException if there was an issue
     * @throws IllegalArgumentException if parsing failed or the tree ID was 
     * invalid or validation failed
     * @throws IllegalStateException if the data hasn't changed. This is OK!
     * @throws JSONException if the object could not be serialized
     */
    public Deferred<Boolean> syncToStorage(final TSDB tsdb, final boolean overwrite) {
        if (tree_id < 1 || tree_id > 65535) {
            throw new IllegalArgumentException("Invalid Tree ID");
        }

        // if there aren't any changes, save time and bandwidth by not writing to
        // storage
        boolean has_changes = false;
        for (Map.Entry<String, Boolean> entry : changed.entrySet()) {
            if (entry.getValue()) {
                has_changes = true;
                break;
            }
        }

        if (!has_changes) {
            LOG.trace(this + " does not have changes, skipping sync to storage");
            throw new IllegalStateException("No changes detected in the rule");
        }

        /**
         * Executes the CAS after retrieving existing rule from storage, if it
         * exists.
         */
        final class StoreCB implements Callback<Deferred<Boolean>, TreeRule> {
            final TreeRule local_rule;

            public StoreCB(final TreeRule local_rule) {
                this.local_rule = local_rule;
            }

            /**
             * @return True if the CAS was successful, false if not
             */
            @Override
            public Deferred<Boolean> call(final TreeRule fetched_rule) {

                TreeRule stored_rule = fetched_rule;
                final byte[] original_rule = stored_rule == null ? new byte[0] : JSON.serializeToBytes(stored_rule);
                if (stored_rule == null) {
                    stored_rule = local_rule;
                } else {
                    if (!stored_rule.copyChanges(local_rule, overwrite)) {
                        LOG.debug(this + " does not have changes, skipping sync to storage");
                        throw new IllegalStateException("No changes detected in the rule");
                    }
                }

                // reset the local change map so we don't keep writing on subsequent
                // requests
                initializeChangedMap();

                // validate before storing
                stored_rule.validateRule();

                final PutRequest put = new PutRequest(tsdb.treeTable(), Tree.idToBytes(tree_id), Tree.TREE_FAMILY(),
                        getQualifier(level, order), JSON.serializeToBytes(stored_rule));
                return tsdb.getClient().compareAndSet(put, original_rule);
            }

        }

        // start the callback chain by fetching from storage
        return fetchRule(tsdb, tree_id, level, order).addCallbackDeferring(new StoreCB(this));
    }

    /**
     * Parses a rule from the given column. Used by the Tree class when scanning
     * a row for rules.
     * @param column The column to parse
     * @return A valid TreeRule object if parsed successfully
     * @throws IllegalArgumentException if the column was empty
     * @throws JSONException if the object could not be serialized
     */
    public static TreeRule parseFromStorage(final KeyValue column) {
        if (column.value() == null) {
            throw new IllegalArgumentException("Tree rule column value was null");
        }

        final TreeRule rule = JSON.parseToObject(column.value(), TreeRule.class);
        rule.initializeChangedMap();
        return rule;
    }

    /**
     * Attempts to retrieve the specified tree rule from storage.
     * @param tsdb The TSDB to use for storage access
     * @param tree_id ID of the tree the rule belongs to
     * @param level Level where the rule resides
     * @param order Order where the rule resides
     * @return A TreeRule object if found, null if it does not exist
     * @throws HBaseException if there was an issue
     * @throws IllegalArgumentException if the one of the required parameters was
     * missing
     * @throws JSONException if the object could not be serialized
     */
    public static Deferred<TreeRule> fetchRule(final TSDB tsdb, final int tree_id, final int level,
            final int order) {
        if (tree_id < 1 || tree_id > 65535) {
            throw new IllegalArgumentException("Invalid Tree ID");
        }
        if (level < 0) {
            throw new IllegalArgumentException("Invalid rule level");
        }
        if (order < 0) {
            throw new IllegalArgumentException("Invalid rule order");
        }

        // fetch the whole row
        final GetRequest get = new GetRequest(tsdb.treeTable(), Tree.idToBytes(tree_id));
        get.family(Tree.TREE_FAMILY());
        get.qualifier(getQualifier(level, order));

        /**
         * Called after fetching to parse the results
         */
        final class FetchCB implements Callback<Deferred<TreeRule>, ArrayList<KeyValue>> {

            @Override
            public Deferred<TreeRule> call(final ArrayList<KeyValue> row) {
                if (row == null || row.isEmpty()) {
                    return Deferred.fromResult(null);
                }
                return Deferred.fromResult(parseFromStorage(row.get(0)));
            }
        }

        return tsdb.getClient().get(get).addCallbackDeferring(new FetchCB());
    }

    /**
     * Attempts to delete the specified rule from storage
     * @param tsdb The TSDB to use for storage access
     * @param tree_id ID of the tree the rule belongs to
     * @param level Level where the rule resides
     * @param order Order where the rule resides
     * @return A deferred without meaning. The response may be null and should
     * only be used to track completion.
     * @throws HBaseException if there was an issue
     * @throws IllegalArgumentException if the one of the required parameters was
     * missing
     */
    public static Deferred<Object> deleteRule(final TSDB tsdb, final int tree_id, final int level,
            final int order) {
        if (tree_id < 1 || tree_id > 65535) {
            throw new IllegalArgumentException("Invalid Tree ID");
        }
        if (level < 0) {
            throw new IllegalArgumentException("Invalid rule level");
        }
        if (order < 0) {
            throw new IllegalArgumentException("Invalid rule order");
        }

        final DeleteRequest delete = new DeleteRequest(tsdb.treeTable(), Tree.idToBytes(tree_id),
                Tree.TREE_FAMILY(), getQualifier(level, order));
        return tsdb.getClient().delete(delete);
    }

    /**
     * Attempts to delete all rules belonging to the given tree.
     * @param tsdb The TSDB to use for storage access
     * @param tree_id ID of the tree the rules belongs to
     * @return A deferred to wait on for completion. The value has no meaning and
     * may be null.
     * @throws HBaseException if there was an issue
     * @throws IllegalArgumentException if the one of the required parameters was
     * missing
     */
    public static Deferred<Object> deleteAllRules(final TSDB tsdb, final int tree_id) {
        if (tree_id < 1 || tree_id > 65535) {
            throw new IllegalArgumentException("Invalid Tree ID");
        }

        // fetch the whole row
        final GetRequest get = new GetRequest(tsdb.treeTable(), Tree.idToBytes(tree_id));
        get.family(Tree.TREE_FAMILY());

        /**
         * Called after fetching the requested row. If the row is empty, we just
         * return, otherwise we compile a list of qualifiers to delete and submit
         * a single delete request to storage.
         */
        final class GetCB implements Callback<Deferred<Object>, ArrayList<KeyValue>> {

            @Override
            public Deferred<Object> call(final ArrayList<KeyValue> row) throws Exception {
                if (row == null || row.isEmpty()) {
                    return Deferred.fromResult(null);
                }

                final ArrayList<byte[]> qualifiers = new ArrayList<byte[]>(row.size());

                for (KeyValue column : row) {
                    if (column.qualifier().length > RULE_PREFIX.length
                            && Bytes.memcmp(RULE_PREFIX, column.qualifier(), 0, RULE_PREFIX.length) == 0) {
                        qualifiers.add(column.qualifier());
                    }
                }

                final DeleteRequest delete = new DeleteRequest(tsdb.treeTable(), Tree.idToBytes(tree_id),
                        Tree.TREE_FAMILY(), qualifiers.toArray(new byte[qualifiers.size()][]));
                return tsdb.getClient().delete(delete);
            }

        }

        return tsdb.getClient().get(get).addCallbackDeferring(new GetCB());
    }

    /**
     * Parses a string into a rule type enumerator
     * @param type The string to parse
     * @return The type enumerator
     * @throws IllegalArgumentException if the type was empty or invalid
     */
    public static TreeRuleType stringToType(final String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("Rule type was empty");
        } else if (type.toLowerCase().equals("metric")) {
            return TreeRuleType.METRIC;
        } else if (type.toLowerCase().equals("metric_custom")) {
            return TreeRuleType.METRIC_CUSTOM;
        } else if (type.toLowerCase().equals("tagk")) {
            return TreeRuleType.TAGK;
        } else if (type.toLowerCase().equals("tagk_custom")) {
            return TreeRuleType.TAGK_CUSTOM;
        } else if (type.toLowerCase().equals("tagv_custom")) {
            return TreeRuleType.TAGV_CUSTOM;
        } else {
            throw new IllegalArgumentException("Unrecognized rule type");
        }
    }

    /** @return The configured rule column prefix */
    public static byte[] RULE_PREFIX() {
        return RULE_PREFIX;
    }

    /**
     * Completes the column qualifier given a level and order using the configured
     * prefix
     * @param level The level of the rule
     * @param order The order of the rule
     * @return A byte array with the column qualifier
     */
    public static byte[] getQualifier(final int level, final int order) {
        final byte[] suffix = (level + ":" + order).getBytes(CHARSET);
        final byte[] qualifier = new byte[RULE_PREFIX.length + suffix.length];
        System.arraycopy(RULE_PREFIX, 0, qualifier, 0, RULE_PREFIX.length);
        System.arraycopy(suffix, 0, qualifier, RULE_PREFIX.length, suffix.length);
        return qualifier;
    }

    /**
     * Sets or resets the changed map flags
     */
    private void initializeChangedMap() {
        // set changed flags
        changed.put("type", false);
        changed.put("field", false);
        changed.put("custom_field", false);
        changed.put("regex", false);
        changed.put("separator", false);
        changed.put("description", false);
        changed.put("notes", false);
        changed.put("regex_group_idx", false);
        changed.put("display_format", false);
        changed.put("level", false);
        changed.put("order", false);
        // tree_id can't change
    }

    /**
     * Checks that the local rule has valid data, i.e. that for different types
     * of rules, the proper parameters exist. For example, a {@code TAGV_CUSTOM}
     * rule must have a valid {@code field} parameter set.
     * @throws IllegalArgumentException if an invalid combination of parameters
     * is provided
     */
    private void validateRule() {
        if (type == null) {
            throw new IllegalArgumentException("Missing rule type");
        }

        switch (type) {
        case METRIC:
            // nothing to validate
            break;
        case METRIC_CUSTOM:
        case TAGK_CUSTOM:
        case TAGV_CUSTOM:
            if (field == null || field.isEmpty()) {
                throw new IllegalArgumentException("Missing field name required for " + type + " rule");
            }
            if (custom_field == null || custom_field.isEmpty()) {
                throw new IllegalArgumentException("Missing custom field name required for " + type + " rule");
            }
            break;
        case TAGK:
            if (field == null || field.isEmpty()) {
                throw new IllegalArgumentException("Missing field name required for " + type + " rule");
            }
            break;
        default:
            throw new IllegalArgumentException("Invalid rule type");
        }

        if ((regex != null || !regex.isEmpty()) && regex_group_idx < 0) {
            throw new IllegalArgumentException("Invalid regex group index. Cannot be less than 0");
        }
    }

    // GETTERS AND SETTERS ----------------------------

    /** @return the type of rule*/
    public TreeRuleType getType() {
        return type;
    }

    /** @return the name of the field to match on */
    public String getField() {
        return field;
    }

    /** @return the custom_field if matching */
    public String getCustomField() {
        return custom_field;
    }

    /** @return the user supplied, uncompiled regex */
    public String getRegex() {
        return regex;
    }

    /** @return an optional separator*/
    public String getSeparator() {
        return separator;
    }

    /** @return the description of the rule*/
    public String getDescription() {
        return description;
    }

    /** @return the notes */
    public String getNotes() {
        return notes;
    }

    /**  @return the regex_group_idx if using regex group extraction */
    public int getRegexGroupIdx() {
        return regex_group_idx;
    }

    /** @return the display_format */
    public String getDisplayFormat() {
        return display_format;
    }

    /** @return the level where the rule resides*/
    public int getLevel() {
        return level;
    }

    /** @return the order of rule processing within a level */
    public int getOrder() {
        return order;
    }

    /** @return the tree_id */
    public int getTreeId() {
        return tree_id;
    }

    /** @return the compiled_regex */
    @JsonIgnore
    public Pattern getCompiledRegex() {
        return compiled_regex;
    }

    /** @param type The type of rule */
    public void setType(TreeRuleType type) {
        if (this.type != type) {
            changed.put("type", true);
            this.type = type;
        }
    }

    /** @param field The field name for matching */
    public void setField(String field) {
        if (!this.field.equals(field)) {
            changed.put("field", true);
            this.field = field;
        }
    }

    /** @param custom_field The custom field name to set if matching */
    public void setCustomField(String custom_field) {
        if (!this.custom_field.equals(custom_field)) {
            changed.put("custom_field", true);
            this.custom_field = custom_field;
        }
    }

    /** 
     * @param regex Stores AND compiles the regex string for use in processing
     * @throws PatternSyntaxException if the regex is invalid
     */
    public void setRegex(String regex) {
        if (!this.regex.equals(regex)) {
            changed.put("regex", true);
            this.regex = regex;
            if (regex != null && !regex.isEmpty()) {
                this.compiled_regex = Pattern.compile(regex);
            } else {
                this.compiled_regex = null;
            }
        }
    }

    /** @param separator A character or string to separate on */
    public void setSeparator(String separator) {
        if (!this.separator.equals(separator)) {
            changed.put("separator", true);
            this.separator = separator;
        }
    }

    /** @param description A brief description of the rule */
    public void setDescription(String description) {
        if (!this.description.equals(description)) {
            changed.put("description", true);
            this.description = description;
        }
    }

    /** @param notes Optional detailed notes about the rule */
    public void setNotes(String notes) {
        if (!this.notes.equals(notes)) {
            changed.put("notes", true);
            this.notes = notes;
        }
    }

    /** @param regex_group_idx An optional index (start at 0) to use for regex 
     * group extraction. Must be a positive value. */
    public void setRegexGroupIdx(int regex_group_idx) {
        if (this.regex_group_idx != regex_group_idx) {
            changed.put("regex_group_idx", true);
            this.regex_group_idx = regex_group_idx;
        }
    }

    /** @param display_format Optional format string to alter the display name */
    public void setDisplayFormat(String display_format) {
        if (!this.display_format.equals(display_format)) {
            changed.put("display_format", true);
            this.display_format = display_format;
        }
    }

    /** @param level The top level processing order. Must be 0 or greater 
     * @throws IllegalArgumentException if the level was negative */
    public void setLevel(int level) {
        if (level < 0) {
            throw new IllegalArgumentException("Negative levels are not allowed");
        }
        if (this.level != level) {
            changed.put("level", true);
            this.level = level;
        }
    }

    /** @param order The order of processing within a level. 
     * Must be 0 or greater 
     * @throws IllegalArgumentException if the order was negative */
    public void setOrder(int order) {
        if (level < 0) {
            throw new IllegalArgumentException("Negative orders are not allowed");
        }
        if (this.order != order) {
            changed.put("order", true);
            this.order = order;
        }
    }

    /** @param tree_id The tree_id to set */
    public void setTreeId(int tree_id) {
        this.tree_id = tree_id;
    }
}