tsd.client.MetricForm.java Source code

Java tutorial

Introduction

Here is the source code for tsd.client.MetricForm.java

Source

// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  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 tsd.client;

import java.util.ArrayList;
import java.util.Arrays;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

final class MetricForm extends HorizontalPanel implements Focusable {

    public static interface MetricChangeHandler extends EventHandler {
        void onMetricChange(MetricForm widget);
    }

    private static final String TSDB_ID_CLASS = "[-_./a-zA-Z0-9]";
    private static final String TSDB_ID_RE = "^" + TSDB_ID_CLASS + "*$";
    private static final String TSDB_TAGVALUE_RE = "^(\\*?" // a `*' wildcard or nothing
            + "|" + TSDB_ID_CLASS + "+(\\|" + TSDB_ID_CLASS + "+)*)$"; // `foo|bar|...'

    private final EventsHandler events_handler;
    private MetricChangeHandler metric_change_handler;

    private final CheckBox downsample = new CheckBox("Downsample");
    private final ListBox downsampler = new ListBox();
    private final ValidatedTextBox interval = new ValidatedTextBox();
    private final CheckBox rate = new CheckBox("Rate");
    private final CheckBox rate_counter = new CheckBox("Rate Ctr");
    private final TextBox counter_max = new TextBox();
    private final TextBox counter_reset_value = new TextBox();
    private final CheckBox x1y2 = new CheckBox("Right Axis");
    private final ListBox aggregators = new ListBox();
    private final ValidatedTextBox metric = new ValidatedTextBox();
    private final FlexTable tagtable = new FlexTable();

    public MetricForm(final EventsHandler handler) {
        events_handler = handler;
        setupDownsampleWidgets();
        downsample.addClickHandler(handler);
        downsampler.addChangeHandler(handler);
        interval.addBlurHandler(handler);
        interval.addKeyPressHandler(handler);
        rate.addClickHandler(handler);
        rate_counter.addClickHandler(handler);
        counter_max.addBlurHandler(handler);
        counter_max.addKeyPressHandler(handler);
        counter_reset_value.addBlurHandler(handler);
        counter_reset_value.addKeyPressHandler(handler);
        x1y2.addClickHandler(handler);
        aggregators.addChangeHandler(handler);
        metric.addBlurHandler(handler);
        metric.addKeyPressHandler(handler);
        {
            final EventsHandler metric_handler = new EventsHandler() {
                protected <H extends EventHandler> void onEvent(final DomEvent<H> event) {
                    if (metric_change_handler != null) {
                        metric_change_handler.onMetricChange(MetricForm.this);
                    }
                }
            };
            metric.addBlurHandler(metric_handler);
            metric.addKeyPressHandler(metric_handler);
        }

        metric.setValidationRegexp(TSDB_ID_RE);
        assembleUi();
    }

    public String getMetric() {
        return metric.getText();
    }

    /**
     * Parses the metric and tags out of the given string.
     * @param metric A string of the form "metric" or "metric{tag=value,...}".
     * @return The name of the metric.
     */
    private String parseWithMetric(final String metric) {
        // TODO: Try to reduce code duplication with Tags.parseWithMetric().
        final int curly = metric.indexOf('{');
        if (curly < 0) {
            clearTags();
            return metric;
        }
        final int len = metric.length();
        if (metric.charAt(len - 1) != '}') { // "foo{"
            clearTags();
            return null; // Missing '}' at the end.
        } else if (curly == len - 2) { // "foo{}"
            clearTags();
            return metric.substring(0, len - 2);
        }
        // substring the tags out of "foo{a=b,...,x=y}" and parse them.
        int i = 0; // Tag index.
        final int num_tags_before = getNumTags();
        for (final String tag : metric.substring(curly + 1, len - 1).split(",")) {
            final String[] kv = tag.split("=");
            if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) {
                setTag(i, "", "");
                continue; // Invalid tag.
            }
            if (i < num_tags_before) {
                setTag(i, kv[0], kv[1]);
            } else {
                addTag(kv[0], kv[1]);
            }
            i++;
        }
        // Leave an empty line at the end.
        if (i < num_tags_before) {
            setTag(i, "", "");
        } else {
            addTag();
        }
        // Remove extra tags.
        for (i++; i < num_tags_before; i++) {
            tagtable.removeRow(i + 1);
        }
        // Return the "foo" part of "foo{a=b,...,x=y}"
        return metric.substring(0, curly);
    }

    public void updateFromQueryString(final String m, final String o) {
        // TODO: Try to reduce code duplication with GraphHandler.parseQuery().
        // m is of the following forms:
        //  agg:[interval-agg:][rate[{counter[,max[,reset]]}:]metric[{tag=value,...}]
        // Where the parts in square brackets `[' .. `]' are optional.
        final String[] parts = m.split(":");
        int i = parts.length;
        if (i < 2 || i > 4) {
            return; // Malformed.
        }

        setSelectedItem(aggregators, parts[0]);

        i--; // Move to the last part (the metric name).
        metric.setText(parseWithMetric(parts[i]));
        metric_change_handler.onMetricChange(this);

        final boolean rate = parts[--i].startsWith("rate");
        this.rate.setValue(rate, false);
        LocalRateOptions rate_options = parseRateOptions(rate, parts[i]);
        this.rate_counter.setValue(rate_options.is_counter, false);
        final long rate_counter_max = rate_options.counter_max;
        this.counter_max.setValue(rate_counter_max == Long.MAX_VALUE ? "" : Long.toString(rate_counter_max), false);
        this.counter_reset_value.setValue(Long.toString(rate_options.reset_value), false);
        if (rate) {
            i--;
        }

        // downsampling function & interval.
        if (i > 0) {
            final int dash = parts[1].indexOf('-', 1); // 1st char can't be `-'.
            if (dash < 0) {
                disableDownsample();
                return; // Invalid downsampling specifier.
            }
            downsample.setValue(true, false);

            downsampler.setEnabled(true);
            setSelectedItem(downsampler, parts[1].substring(dash + 1));

            interval.setEnabled(true);
            interval.setText(parts[1].substring(0, dash));
        } else {
            disableDownsample();
        }

        x1y2.setValue(o.contains("axis x1y2"), false);
    }

    private void disableDownsample() {
        downsample.setValue(false, false);
        interval.setEnabled(false);
        downsampler.setEnabled(false);
    }

    public CheckBox x1y2() {
        return x1y2;
    }

    private void assembleUi() {
        setWidth("100%");
        { // Left hand-side panel.
            final HorizontalPanel hbox = new HorizontalPanel();
            final InlineLabel l = new InlineLabel();
            l.setText("Metric:");
            hbox.add(l);
            final SuggestBox suggest = RemoteOracle.newSuggestBox("metrics", metric);
            suggest.setLimit(40);
            hbox.add(suggest);
            hbox.setWidth("100%");
            metric.setWidth("100%");

            tagtable.setWidget(0, 0, hbox);
            tagtable.getFlexCellFormatter().setColSpan(0, 0, 3);
            addTag();
            tagtable.setText(1, 0, "Tags");
            add(tagtable);
        }
        { // Right hand-side panel.
            final VerticalPanel vbox = new VerticalPanel();
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                hbox.add(rate);
                hbox.add(rate_counter);
                hbox.add(x1y2);
                vbox.add(hbox);
            }
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                final InlineLabel l = new InlineLabel("Rate Ctr Max:");
                hbox.add(l);
                hbox.add(counter_max);
                vbox.add(hbox);
            }
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                final InlineLabel l = new InlineLabel("Rate Ctr Reset:");
                hbox.add(l);
                hbox.add(counter_reset_value);
                vbox.add(hbox);
            }
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                final InlineLabel l = new InlineLabel();
                l.setText("Aggregator:");
                hbox.add(l);
                hbox.add(aggregators);
                vbox.add(hbox);
            }
            vbox.add(downsample);
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                hbox.add(downsampler);
                hbox.add(interval);
                vbox.add(hbox);
            }
            add(vbox);
        }
    }

    public void setMetricChangeHandler(final MetricChangeHandler handler) {
        metric_change_handler = handler;
    }

    public void setAggregators(final ArrayList<String> aggs) {
        Object[] aggsSortedArray = aggs.toArray();
        Arrays.sort(aggsSortedArray);
        for (final Object agg : aggsSortedArray) {
            aggregators.addItem((String) agg);
            downsampler.addItem((String) agg);
        }
        setSelectedItem(aggregators, "sum");
        setSelectedItem(downsampler, "avg");
    }

    public boolean buildQueryString(final StringBuilder url) {
        final String metric = getMetric();
        if (metric.isEmpty()) {
            return false;
        }
        url.append("&m=");
        url.append(selectedValue(aggregators));
        if (downsample.getValue()) {
            url.append(':').append(interval.getValue()).append('-').append(selectedValue(downsampler));
        }
        if (rate.getValue()) {
            url.append(":rate");
            if (rate_counter.getValue()) {
                url.append('{').append("counter");
                final String max = counter_max.getValue().trim();
                final String reset = counter_reset_value.getValue().trim();
                if (max.length() > 0 && reset.length() > 0) {
                    url.append(',').append(max).append(',').append(reset);
                } else if (max.length() > 0 && reset.length() == 0) {
                    url.append(',').append(max);
                } else if (max.length() == 0 && reset.length() > 0) {
                    url.append(",,").append(reset);
                }
                url.append('}');
            }
        }
        url.append(':').append(metric);
        {
            final int ntags = getNumTags();
            url.append('{');
            for (int tag = 0; tag < ntags; tag++) {
                final String tagname = getTagName(tag);
                final String tagvalue = getTagValue(tag);
                if (tagname.isEmpty() || tagvalue.isEmpty()) {
                    continue;
                }
                url.append(tagname).append('=').append(tagvalue).append(',');
            }
            final int last = url.length() - 1;
            if (url.charAt(last) == '{') { // There was no tag.
                url.setLength(last); // So remove the `{'.
            } else { // Need to replace the last `,' with a `}'.
                url.setCharAt(url.length() - 1, '}');
            }
        }
        url.append("&o=");
        if (x1y2.getValue()) {
            url.append("axis x1y2");
        }
        return true;
    }

    private int getNumTags() {
        return tagtable.getRowCount() - 1;
    }

    private String getTagName(final int i) {
        return ((SuggestBox) tagtable.getWidget(i + 1, 1)).getValue();
    }

    private String getTagValue(final int i) {
        return ((SuggestBox) tagtable.getWidget(i + 1, 2)).getValue();
    }

    private void setTagName(final int i, final String value) {
        ((SuggestBox) tagtable.getWidget(i + 1, 1)).setValue(value);
    }

    private void setTagValue(final int i, final String value) {
        ((SuggestBox) tagtable.getWidget(i + 1, 2)).setValue(value);
    }

    /**
     * Changes the name/value of an existing tag.
     * @param i The index of the tag to change.
     * @param name The new name of the tag.
     * @param value The new value of the tag.
     * Requires: {@code i < getNumTags()}.
     */
    private void setTag(final int i, final String name, final String value) {
        setTagName(i, name);
        setTagValue(i, value);
    }

    private void addTag() {
        addTag(null, null);
    }

    private void addTag(final String default_tagname) {
        addTag(default_tagname, null);
    }

    private void addTag(final String default_tagname, final String default_value) {
        final int row = tagtable.getRowCount();

        final ValidatedTextBox tagname = new ValidatedTextBox();
        final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname);
        final ValidatedTextBox tagvalue = new ValidatedTextBox();
        final SuggestBox suggesttagv = RemoteOracle.newSuggestBox("tagv", tagvalue);
        tagname.setValidationRegexp(TSDB_ID_RE);
        tagvalue.setValidationRegexp(TSDB_TAGVALUE_RE);
        tagname.setWidth("100%");
        tagvalue.setWidth("100%");
        tagname.addBlurHandler(recompact_tagtable);
        tagname.addBlurHandler(events_handler);
        tagname.addKeyPressHandler(events_handler);
        tagvalue.addBlurHandler(recompact_tagtable);
        tagvalue.addBlurHandler(events_handler);
        tagvalue.addKeyPressHandler(events_handler);

        tagtable.setWidget(row, 1, suggesttagk);
        tagtable.setWidget(row, 2, suggesttagv);
        if (row > 2) {
            final Button remove = new Button("x");
            remove.addClickHandler(removetag);
            tagtable.setWidget(row - 1, 0, remove);
        }
        if (default_tagname != null) {
            tagname.setText(default_tagname);
            if (default_value == null) {
                tagvalue.setFocus(true);
            }
        }
        if (default_value != null) {
            tagvalue.setText(default_value);
        }
    }

    private void clearTags() {
        setTag(0, "", "");
        for (int i = getNumTags() - 1; i > 1; i++) {
            tagtable.removeRow(i + 1);
        }
    }

    public void autoSuggestTag(final String tag) {
        // First try to see if the tag is already in the table.
        final int nrows = tagtable.getRowCount();
        int unused_row = -1;
        for (int row = 1; row < nrows; row++) {
            final SuggestBox tagname = ((SuggestBox) tagtable.getWidget(row, 1));
            final SuggestBox tagvalue = ((SuggestBox) tagtable.getWidget(row, 2));
            final String thistag = tagname.getValue();
            if (thistag.equals(tag)) {
                return; // This tag is already in the table.
            }
            if (thistag.isEmpty() && tagvalue.getValue().isEmpty()) {
                unused_row = row;
                break;
            }
        }
        if (unused_row >= 0) {
            ((SuggestBox) tagtable.getWidget(unused_row, 1)).setValue(tag);
        } else {
            addTag(tag);
        }
    }

    private final BlurHandler recompact_tagtable = new BlurHandler() {
        public void onBlur(final BlurEvent event) {
            int ntags = getNumTags();
            // Is the first line empty?  If yes, move everything up by 1 line.
            if (getTagName(0).isEmpty() && getTagValue(0).isEmpty()) {
                for (int tag = 1; tag < ntags; tag++) {
                    final String tagname = getTagName(tag);
                    final String tagvalue = getTagValue(tag);
                    setTag(tag - 1, tagname, tagvalue);
                }
                setTag(ntags - 1, "", "");
            }
            // Try to remove empty lines from the tag table (but never remove the
            // first line or last line, even if they're empty).  Walk the table
            // from the end to make it easier to delete rows as we iterate.
            for (int tag = ntags - 1; tag >= 1; tag--) {
                final String tagname = getTagName(tag);
                final String tagvalue = getTagValue(tag);
                if (tagname.isEmpty() && tagvalue.isEmpty()) {
                    tagtable.removeRow(tag + 1);
                }
            }
            ntags = getNumTags(); // How many lines are left?
            // If the last line isn't empty, add another one.
            final String tagname = getTagName(ntags - 1);
            final String tagvalue = getTagValue(ntags - 1);
            if (!tagname.isEmpty() && !tagvalue.isEmpty()) {
                addTag();
            }
        }
    };

    private final ClickHandler removetag = new ClickHandler() {
        public void onClick(final ClickEvent event) {
            if (!(event.getSource() instanceof Button)) {
                return;
            }
            final Widget source = (Widget) event.getSource();
            final int ntags = getNumTags();
            for (int tag = 1; tag < ntags; tag++) {
                if (source == tagtable.getWidget(tag + 1, 0)) {
                    tagtable.removeRow(tag + 1);
                    events_handler.onClick(event);
                    break;
                }
            }
        }
    };

    private void setupDownsampleWidgets() {
        downsampler.setEnabled(false);
        interval.setEnabled(false);
        interval.setMaxLength(5);
        interval.setVisibleLength(5);
        interval.setValue("10m");
        interval.setValidationRegexp("^[1-9][0-9]*[smhdwy]$");
        downsample.addClickHandler(new ClickHandler() {
            public void onClick(final ClickEvent event) {
                final boolean checked = ((CheckBox) event.getSource()).getValue();
                downsampler.setEnabled(checked);
                interval.setEnabled(checked);
                if (checked) {
                    downsampler.setFocus(true);
                }
            }
        });
    }

    private static String selectedValue(final ListBox list) { // They should add
        return list.getValue(list.getSelectedIndex()); // this to GWT...
    }

    /**
     * If the given item is in the list, mark it as selected.
     * @param list The list to manipulate.
     * @param item The item to select if present.
     */
    private void setSelectedItem(final ListBox list, final String item) {
        final int nitems = list.getItemCount();
        for (int i = 0; i < nitems; i++) {
            if (item.equals(list.getValue(i))) {
                list.setSelectedIndex(i);
                return;
            }
        }
    }

    /**
     * Class used for parsing and rate options
     */
    private static class LocalRateOptions {
        public boolean is_counter;
        public long counter_max = Long.MAX_VALUE;
        public long reset_value = 0;
    }

    /**
     * Parses the "rate" section of the query string and returns an instance
     * of the LocalRateOptions class that contains the values found.
     * <p/>
     * The format of the rate specification is rate[{counter[,#[,#]]}].
     * If the spec is invalid or we were unable to parse properly, it returns a
     * default options object.
     * @param rate If true, then the query is set as a rate query and the rate
     * specification will be parsed. If false, a default RateOptions instance
     * will be returned and largely ignored by the rest of the processing
     * @param spec The part of the query string that pertains to the rate
     * @return An initialized LocalRateOptions instance based on the specification
     * @since 2.0
     */
    static final public LocalRateOptions parseRateOptions(boolean rate, String spec) {
        if (!rate || spec.length() < 6) {
            return new LocalRateOptions();
        }

        String[] parts = spec.split(spec.substring(5, spec.length() - 1), ',');
        if (parts.length < 1 || parts.length > 3) {
            return new LocalRateOptions();
        }

        try {
            LocalRateOptions options = new LocalRateOptions();
            options.is_counter = "counter".equals(parts[0]);
            options.counter_max = (parts.length >= 2 && parts[1].length() > 0 ? Long.parseLong(parts[1])
                    : Long.MAX_VALUE);
            options.reset_value = (parts.length >= 3 && parts[2].length() > 0 ? Long.parseLong(parts[2]) : 0);
            return options;
        } catch (NumberFormatException e) {
            return new LocalRateOptions();
        }
    }

    // ------------------- //
    // Focusable interface //
    // ------------------- //

    public int getTabIndex() {
        return metric.getTabIndex();
    }

    public void setTabIndex(final int index) {
        metric.setTabIndex(index);
    }

    public void setAccessKey(final char key) {
        metric.setAccessKey(key);
    }

    public void setFocus(final boolean focused) {
        metric.setFocus(focused);
    }

}