Package net.opentsdb.tree

Source Code of net.opentsdb.tree.LeafCB

// 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.xml.bind.DatatypeConverter;

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

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.core.JsonGenerator;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import com.stumbleupon.async.DeferredGroupException;

import net.opentsdb.core.TSDB;
import net.opentsdb.uid.NoSuchUniqueId;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.utils.JSON;
import net.opentsdb.utils.JSONException;

/**
* Represents a branch of a meta data tree, used to organize timeseries into
* a hierarchy for easy navigation. Each branch is composed of itself and
* potential child branches and/or child leaves.
* <p>
* Branch IDs are hex encoded byte arrays composed of the tree ID + hash of
* the display name for each previous branch. The tree ID is encoded on
* {@link Tree#TREE_ID_WIDTH()} bytes, each hash is then {@code INT_WIDTH}
* bytes. So the if the tree ID width is 2 bytes and Java Integers are 4 bytes,
* the root for tree # 1 is just {@code 0001}. A child of the root could be
* {@code 00001A3B190C2} and so on. These IDs are used as the row key in storage.
* <p>
* Branch definitions are JSON objects stored in the "branch" column of the
* branch ID row. Only the tree ID, path and display name are stored in the
* definition column to keep space down. Leaves are stored in separate columns
* and child branch definitions are stored in separate rows. Note that the root
* branch definition for a tree will be stored in the same row as the tree
* definition since they share the same row key.
* <p>
* When fetching a branch with children and leaves, a scanner is
* configured with a row key regex to scan any rows that match the branch ID
* plus an additional {@code INT_WIDTH} so that when we scan, we can pick up all
* of the rows with child branch definitions. Also, when loading a full branch,
* any leaves for the request branch can load the associated UID names from
* storage, so this can get expensive. Leaves for a child branch will not be
* loaded, only leaves that belong directly to the local will. Also, children
* branches of children will not be loaded. We only return one branch at a
* time since the tree could be HUGE!
* <p>
* Storing a branch will only write the definition column for the local branch
* object. Child branches will not be written to storage. If you've loaded
* and modified children in this branch, you need to loop through the children
* and store them individually. Leaves belonging to this branch will be stored
* and collisions recorded to the given Tree object.
* @since 2.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY)
public final class Branch implements Comparable<Branch> {
  private static final Logger LOG = LoggerFactory.getLogger(Branch.class);
 
  /** Charset used to convert Strings to byte arrays and back. */
  private static final Charset CHARSET = Charset.forName("ISO-8859-1");
  /** Integer width in bytes */
  private static final short INT_WIDTH = 4;
  /** Name of the branch qualifier ID */
  private static final byte[] BRANCH_QUALIFIER = "branch".getBytes(CHARSET);
 
  /** The tree this branch belongs to */
  private int tree_id;

  /** Display name for the branch */
  private String display_name = "";

  /** Hash map of leaves belonging to this branch */
  private HashMap<Integer, Leaf> leaves;
 
  /** Hash map of child branches */
  private TreeSet<Branch> branches;

  /** The path/name of the branch */
  private TreeMap<Integer, String> path;
 
  /**
   * Default empty constructor necessary for de/serialization
   */
  public Branch() {
   
  }

  /**
   * Constructor that sets the tree ID
   * @param tree_id ID of the tree this branch is associated with
   */
  public Branch(final int tree_id) {
    this.tree_id = tree_id;
  }
 
  /**
   * Copy constructor that creates a completely independent copy of the original
   * @param original The original object to copy from
   */
  public Branch(final Branch original) {
    tree_id = original.tree_id;
    display_name = original.display_name;
    if (original.leaves != null) {
      leaves = new HashMap<Integer, Leaf>(original.leaves);
    }
    if (original.branches != null) {
      branches = new TreeSet<Branch>(original.branches);
    }
    if (original.path != null) {
      path = new TreeMap<Integer, String>(original.path);
    }
  }
 
  /** @return Returns the {@code display_name}'s hash code or 0 if it's not set */
  @Override
  public int hashCode() {
    if (display_name == null || display_name.isEmpty()) {
      return 0;
    }
    return display_name.hashCode();
  }
 
  /**
   * Just compares the branch display name
   * @param obj The object to compare this to
   * @return True if the branch IDs are the same or the incoming object is
   * this one
   */
  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (this.getClass() != obj.getClass()) {
      return false;
    }
    if (obj == this) {
      return true;
    }
   
    final Branch branch = (Branch)obj;
    return display_name.equals(branch.display_name);
  }
 
  /**
   * Comparator based on the {@code display_name} to sort branches when
   * returning to an RPC calls
   */
  @Override
  public int compareTo(Branch branch) {
    return this.display_name.compareToIgnoreCase(branch.display_name);
  }
 
  /** @return Information about this branch including ID and display name */
  @Override
  public String toString() {
    if (path == null) {
      return "Name: [" + display_name + "]";
    } else {
      return "ID: [" + getBranchId() + "] Name: [" + display_name + "]";
    }
  }
 
  /**
   * Adds a child branch to the local branch set if it doesn't exist. Also
   * initializes the set if it hasn't been initialized yet
   * @param branch The branch to add
   * @return True if the branch did not exist in the set previously
   * @throws IllegalArgumentException if the incoming branch is null
   */
  public boolean addChild(final Branch branch) {
    if (branch == null) {
      throw new IllegalArgumentException("Null branches are not allowed");
    }
    if (branches == null) {
      branches = new TreeSet<Branch>();
      branches.add(branch);
      return true;
    }
   
    if (branches.contains(branch)) {
      return false;
    }
    branches.add(branch);
    return true;
  }
 
  /**
   * Adds a leaf to the local branch, looking for collisions
   * @param leaf The leaf to add
   * @param tree The tree to report to with collisions
   * @return True if the leaf was new, false if the leaf already exists or
   * would cause a collision
   * @throws IllegalArgumentException if the incoming leaf is null
   */
  public boolean addLeaf(final Leaf leaf, final Tree tree) {
    if (leaf == null) {
      throw new IllegalArgumentException("Null leaves are not allowed");
    }
    if (leaves == null) {
      leaves = new HashMap<Integer, Leaf>();
      leaves.put(leaf.hashCode(), leaf);
      return true;
    }
   
    if (leaves.containsKey(leaf.hashCode())) {
      // if we try to sync a leaf with the same hash of an existing key
      // but a different TSUID, it's a collision, so mark it
      if (!leaves.get(leaf.hashCode()).getTsuid().equals(leaf.getTsuid())) {
        final Leaf collision = leaves.get(leaf.hashCode());
        if (tree != null) {
          tree.addCollision(leaf.getTsuid(), collision.getTsuid());
        }
       
        // log at info or lower since it's not a system error, rather it's
        // a user issue with the rules or naming schema
        LOG.warn("Incoming TSUID [" + leaf.getTsuid() +
            "] collided with existing TSUID [" + collision.getTsuid() +
            "] on display name [" + collision.getDisplayName() + "]");
      }
      return false;
    } else {
      leaves.put(leaf.hashCode(), leaf);
      return true;
    }
  }
 
  /**
   * Attempts to compile the branch ID for this branch. In order to successfully
   * compile, the {@code tree_id}, {@code path} and {@code display_name} must
   * be set. The path may be empty, which indicates this is a root branch, but
   * it must be a valid Map object.
   * @return The branch ID as a byte array
   * @throws IllegalArgumentException if any required parameters are missing
   */
  public byte[] compileBranchId() {
    if (tree_id < 1 || tree_id > 65535) {
      throw new IllegalArgumentException("Missing or invalid tree ID");
    }
    // root branch path may be empty
    if (path == null) {
      throw new IllegalArgumentException("Missing branch path");
    }
    if (display_name == null || display_name.isEmpty()) {
      throw new IllegalArgumentException("Missing display name");
    }
   
    // first, make sure the display name is at the tip of the tree set
    if (path.isEmpty()) {
      path.put(0, display_name);
    } else if (!path.lastEntry().getValue().equals(display_name)) {
      final int depth = path.lastEntry().getKey() + 1;
      path.put(depth, display_name);
    }
   
    final byte[] branch_id = new byte[Tree.TREE_ID_WIDTH() +
                                      ((path.size() - 1) * INT_WIDTH)];
    int index = 0;
    final byte[] tree_bytes = Tree.idToBytes(tree_id);
    System.arraycopy(tree_bytes, 0, branch_id, index, tree_bytes.length);
    index += tree_bytes.length;
   
    for (Map.Entry<Integer, String> entry : path.entrySet()) {
      // skip the root, keeps the row keys 4 bytes shorter
      if (entry.getKey() == 0) {
        continue;
      }
     
      final byte[] hash = Bytes.fromInt(entry.getValue().hashCode());
      System.arraycopy(hash, 0, branch_id, index, hash.length);
      index += hash.length;
    }
   
    return branch_id;
  }
 
  /**
   * Sets the path for this branch based off the path of the parent. This map
   * may be empty, in which case the branch is considered a root.
   * <b>Warning:</b> If the path has already been set, this will create a new
   * path, clearing out any existing entries
   * @param parent_path The map to store as the path
   * @throws IllegalArgumentException if the parent path is null
   */
  public void prependParentPath(final Map<Integer, String> parent_path) {
    if (parent_path == null) {
      throw new IllegalArgumentException("Parent path was null");
    }
    path = new TreeMap<Integer, String>();
    path.putAll(parent_path);
  }
 
  /**
   * Attempts to write the branch definition and optionally child leaves to
   * storage via CompareAndSets.
   * Each returned deferred will be a boolean regarding whether the CAS call
   * was successful or not. This will be a mix of the branch call and leaves.
   * Some of these may be false, which is OK, because if the branch
   * definition already exists, we don't need to re-write it. Leaves will
   * return false if there was a collision.
   * @param tsdb The TSDB to use for access
   * @param tree The tree to record collisions to
   * @param store_leaves Whether or not child leaves should be written to
   * storage
   * @return A list of deferreds to wait on for completion.
   * @throws HBaseException if there was an issue
   * @throws IllegalArgumentException if the tree ID was missing or data was
   * missing
   */
  public Deferred<ArrayList<Boolean>> storeBranch(final TSDB tsdb,
      final Tree tree, final boolean store_leaves) { 
    if (tree_id < 1 || tree_id > 65535) {
      throw new IllegalArgumentException("Missing or invalid tree ID");
    }

    final ArrayList<Deferred<Boolean>> storage_results =
      new ArrayList<Deferred<Boolean>>(leaves != null ? leaves.size() + 1 : 1);
   
    // compile the row key by making sure the display_name is in the path set
    // row ID = <treeID>[<parent.display_name.hashCode()>...]
    final byte[] row = this.compileBranchId();
   
    // compile the object for storage, this will toss exceptions if we are
    // missing anything important
    final byte[] storage_data = toStorageJson();

    final PutRequest put = new PutRequest(tsdb.treeTable(), row, Tree.TREE_FAMILY(),
        BRANCH_QUALIFIER, storage_data);
    put.setBufferable(true);
    storage_results.add(tsdb.getClient().compareAndSet(put, new byte[0]));
   
    // store leaves if told to and put the storage calls in our deferred group
    if (store_leaves && leaves != null && !leaves.isEmpty()) {
      for (final Leaf leaf : leaves.values()) {
        storage_results.add(leaf.storeLeaf(tsdb, row, tree));
      }
    }
   
    return Deferred.group(storage_results);
  }
 
  /**
   * Attempts to fetch only the branch definition object from storage. This is
   * much faster than scanning many rows for child branches as per the
   * {@link #fetchBranch} call. Useful when building trees, particularly to
   * fetch the root branch.
   * @param tsdb The TSDB to use for access
   * @param branch_id ID of the branch to retrieve
   * @return A branch if found, null if it did not exist
   * @throws JSONException if the object could not be deserialized
   */
  public static Deferred<Branch> fetchBranchOnly(final TSDB tsdb,
      final byte[] branch_id) {
   
    final GetRequest get = new GetRequest(tsdb.treeTable(), branch_id);
    get.family(Tree.TREE_FAMILY());
    get.qualifier(BRANCH_QUALIFIER);
   
    /**
     * Called after the get returns with or without data. If we have data, we'll
     * parse the branch and return it.
     */
    final class GetCB implements Callback<Deferred<Branch>, ArrayList<KeyValue>> {

      @Override
      public Deferred<Branch> call(ArrayList<KeyValue> row) throws Exception {
        if (row == null || row.isEmpty()) {
          return Deferred.fromResult(null);
        }
       
        final Branch branch = JSON.parseToObject(row.get(0).value(),
            Branch.class);
       
        // WARNING: Since the json doesn't store the tree ID, to cut down on
        // space, we have to load it from the row key.
        branch.tree_id = Tree.bytesToId(row.get(0).key());
        return Deferred.fromResult(branch);
      }
     
    }
   
    return tsdb.getClient().get(get).addCallbackDeferring(new GetCB());
  }
 
  /**
   * Attempts to fetch the branch, it's leaves and all child branches.
   * The UID names for each leaf may also be loaded if configured.
   * @param tsdb The TSDB to use for storage access
   * @param branch_id ID of the branch to retrieve
   * @param load_leaf_uids Whether or not to load UID names for each leaf
   * @return A branch if found, null if it did not exist
   * @throws JSONException if the object could not be deserialized
   */
  public static Deferred<Branch> fetchBranch(final TSDB tsdb,
      final byte[] branch_id, final boolean load_leaf_uids) {
   
    final Deferred<Branch> result = new Deferred<Branch>();
    final Scanner scanner = setupBranchScanner(tsdb, branch_id);
   
    // This is the branch that will be loaded with data from the scanner and
    // returned at the end of the process.
    final Branch branch = new Branch();
   
    // A list of deferreds to wait on for child leaf processing
    final ArrayList<Deferred<Object>> leaf_group =
      new ArrayList<Deferred<Object>>();
   
    /**
     * Exception handler to catch leaves with an invalid UID name due to a
     * possible deletion. This will allow the scanner to keep loading valid
     * leaves and ignore problems. The fsck tool can be used to clean up
     * orphaned leaves. If we catch something other than an NSU, it will
     * re-throw the exception
     */
    final class LeafErrBack implements Callback<Object, Exception> {

      final byte[] qualifier;
     
      public LeafErrBack(final byte[] qualifier) {
        this.qualifier = qualifier;
      }
     
      @Override
      public Object call(final Exception e) throws Exception {
        Throwable ex = e;
        while (ex.getClass().equals(DeferredGroupException.class)) {
          ex = ex.getCause();
        }
        if (ex.getClass().equals(NoSuchUniqueId.class)) {
          LOG.debug("Invalid UID for leaf: " + idToString(qualifier) +
              " in branch: " + idToString(branch_id), ex);
        } else {
          throw (Exception)ex;
        }
        return null;
      }
     
    }
   
    /**
     * Called after a leaf has been loaded successfully and adds the leaf
     * to the branch's leaf set. Also lazily initializes the leaf set if it
     * hasn't been.
     */
    final class LeafCB implements Callback<Object, Leaf> {

      @Override
      public Object call(final Leaf leaf) throws Exception {
        if (leaf != null) {
          if (branch.leaves == null) {
            branch.leaves = new HashMap<Integer, Leaf>();
          }
          branch.leaves.put(leaf.hashCode(), leaf);
        }
        return null;
      }
     
    }
   
    /**
     * Scanner callback executed recursively each time we get a set of data
     * from storage. This is responsible for determining what columns are
     * returned and issuing requests to load leaf objects.
     * When the scanner returns a null set of rows, the method initiates the
     * final callback.
     */
    final class FetchBranchCB implements Callback<Object,
      ArrayList<ArrayList<KeyValue>>> {
 
      /**
       * Starts the scanner and is called recursively to fetch the next set of
       * rows from the scanner.
       * @return The branch if loaded successfully, null if the branch was not
       * found.
       */
      public Object fetchBranch() {
        return scanner.nextRows().addCallback(this);
      }
     
      /**
       * Loops through each row of the scanner results and parses out branch
       * definitions and child leaves.
       * @return The final branch callback if the scanner returns a null set
       */
      @Override
      public Object call(final ArrayList<ArrayList<KeyValue>> rows)
          throws Exception {
        if (rows == null) {
          if (branch.tree_id < 1 || branch.path == null) {
            result.callback(null);
          } else {
            result.callback(branch);
          }
          return null;
        }
       
        for (final ArrayList<KeyValue> row : rows) {
          for (KeyValue column : row) {

            // matched a branch column
            if (Bytes.equals(BRANCH_QUALIFIER, column.qualifier())) {
              if (Bytes.equals(branch_id, column.key())) {
               
                // it's *this* branch. We deserialize to a new object and copy
                // since the columns could be in any order and we may get a
                // leaf before the branch
                final Branch local_branch = JSON.parseToObject(column.value(),
                    Branch.class);
                branch.path = local_branch.path;
                branch.display_name = local_branch.display_name;
                branch.tree_id = Tree.bytesToId(column.key());

              } else {
                // it's a child branch
                final Branch child = JSON.parseToObject(column.value(),
                    Branch.class);
                child.tree_id = Tree.bytesToId(column.key());
                branch.addChild(child);
              }
            // parse out a leaf
            } else if (Bytes.memcmp(Leaf.LEAF_PREFIX(), column.qualifier(), 0,
                Leaf.LEAF_PREFIX().length) == 0) {
              if (Bytes.equals(branch_id, column.key())) {
                // process a leaf and skip if the UIDs for the TSUID can't be
                // found. Add an errback to catch NoSuchUniqueId exceptions
                leaf_group.add(Leaf.parseFromStorage(tsdb, column,
                    load_leaf_uids)
                    .addCallbacks(new LeafCB(),
                        new LeafErrBack(column.qualifier())));
              } else {
                // TODO - figure out an efficient way to increment a counter in
                // the child branch with the # of leaves it has
              }
            }
          }
        }
       
        // recursively call ourself to fetch more results from the scanner
        return fetchBranch();
      }     
    }
   
    // start scanning
    new FetchBranchCB().fetchBranch();
    return result;
  }
 
  /**
   * Converts a branch ID hash to a hex encoded, upper case string with padding
   * @param branch_id The ID to convert
   * @return the branch ID as a character hex string
   */
  public static String idToString(final byte[] branch_id) {
    return DatatypeConverter.printHexBinary(branch_id);
  }
 
  /**
   * Converts a hex string to a branch ID byte array (row key)
   * @param branch_id The branch ID to convert
   * @return The branch ID as a byte array
   * @throws IllegalArgumentException if the string is not valid hex
   */
  public static byte[] stringToId(final String branch_id) {
    if (branch_id == null || branch_id.isEmpty()) {
      throw new IllegalArgumentException("Branch ID was empty");
    }
    if (branch_id.length() < 4) {
      throw new IllegalArgumentException("Branch ID was too short");
    }
    String id = branch_id;
    if (id.length() % 2 != 0) {
      id = "0" + id;
    }
    return DatatypeConverter.parseHexBinary(id);
  }

  /** @return The branch column qualifier name */
  public static byte[] BRANCH_QUALIFIER() {
    return BRANCH_QUALIFIER;
  }
  /**
   * Returns serialized data for the branch to put in storage. This is necessary
   * to reduce storage space and for proper CAS calls
   * @return A byte array for storage
   */
  private byte[] toStorageJson() {
    // grab some memory to avoid reallocs
    final ByteArrayOutputStream output = new ByteArrayOutputStream(
        (display_name.length() * 2) + (path.size() * 128));
    try {
      final JsonGenerator json = JSON.getFactory().createGenerator(output);
     
      json.writeStartObject();
     
      // we only need to write a small amount of information
      json.writeObjectField("path", path);
      json.writeStringField("displayName", display_name);
     
      json.writeEndObject();
      json.close();
     
      // TODO zero copy?
      return output.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Configures an HBase scanner to fetch the requested branch and all child
   * branches. It uses a row key regex filter to match any rows starting with
   * the given branch and another INT_WIDTH bytes deep. Deeper branches are
   * ignored.
   * @param tsdb The TSDB to use for storage access
   * @param branch_id ID of the branch to fetch
   * @return An HBase scanner ready for scanning
   */
  private static Scanner setupBranchScanner(final TSDB tsdb,
      final byte[] branch_id) {
    final byte[] start = branch_id;
    final byte[] end = Arrays.copyOf(branch_id, branch_id.length);
    final Scanner scanner = tsdb.getClient().newScanner(tsdb.treeTable());
    scanner.setStartKey(start);
   
    // increment the tree ID so we scan the whole tree
    byte[] tree_id = new byte[INT_WIDTH];
    for (int i = 0; i < Tree.TREE_ID_WIDTH(); i++) {
      tree_id[i + (INT_WIDTH - Tree.TREE_ID_WIDTH())] = end[i];
    }
    int id = Bytes.getInt(tree_id) + 1;
    tree_id = Bytes.fromInt(id);
    for (int i = 0; i < Tree.TREE_ID_WIDTH(); i++) {
      end[i] = tree_id[i + (INT_WIDTH - Tree.TREE_ID_WIDTH())];
    }
    scanner.setStopKey(end);
    scanner.setFamily(Tree.TREE_FAMILY());

    // TODO - use the column filter to fetch only branches and leaves, ignore
    // collisions, no matches and other meta
   
    // set the regex filter
    // we want one branch below the current ID so we want something like:
    // {0, 1, 1, 2, 3, 4 }  where { 0, 1 } is the tree ID, { 1, 2, 3, 4 } is the
    // branch
    // "^\\Q\000\001\001\002\003\004\\E(?:.{4})$"
   
    final StringBuilder buf = new StringBuilder((start.length * 6) + 20);
    buf.append("(?s)"  // Ensure we use the DOTALL flag.
        + "^\\Q");
    for (final byte b : start) {
      buf.append((char) (b & 0xFF));
    }
    buf.append("\\E(?:.{").append(INT_WIDTH).append("})?$");
   
    scanner.setKeyRegexp(buf.toString(), CHARSET);
    return scanner;
  }
 
  // GETTERS AND SETTERS ----------------------------
 
  /** @return The ID of the tree this branch belongs to */
  public int getTreeId() {
    return tree_id;
  }

  /** @return The ID of this branch */
  public String getBranchId() {
    final byte[] id = compileBranchId();
    if (id == null) {
      return null;
    }
    return UniqueId.uidToString(id);
  }
 
  /** @return The path of the tree */
  public Map<Integer, String> getPath() {
    compileBranchId();
    return path;
  }

  /** @return Depth of this branch */
  public int getDepth() {
    return path.lastKey();
  }

  /** @return Name to display to the public */
  public String getDisplayName() {
    return display_name;
  }

  /** @return Ordered set of leaves belonging to this branch */
  public TreeSet<Leaf> getLeaves() {
    if (leaves == null) {
      return null;
    }
    return new TreeSet<Leaf>(leaves.values());
  }

  /** @return Ordered set of child branches */
  public TreeSet<Branch> getBranches() {
    return branches;
  }

  /** @param tree_id ID of the tree this branch belongs to */
  public void setTreeId(int tree_id) {
    this.tree_id = tree_id;
  }
 
  /** @param display_name Public name to display */
  public void setDisplayName(String display_name) {
    this.display_name = display_name;
  }

}
TOP

Related Classes of net.opentsdb.tree.LeafCB

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.