net.opentsdb.tree.Branch.java Source code

Java tutorial

Introduction

Here is the source code for net.opentsdb.tree.Branch.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.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;
    }

}