All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.deephaven.engine.util.LayoutHintBuilder Maven / Gradle / Ivy

There is a newer version: 0.37.1
Show newest version
/**
 * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
 */
package io.deephaven.engine.util;

import io.deephaven.engine.table.Table;
import io.deephaven.api.util.NameValidator;
import io.deephaven.gui.color.Color;
import io.deephaven.util.annotations.ScriptApi;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.stream.Collectors;

/**
 * The builder class for use in assembling layout hints suitable for use with {@link #applyToTable(Table)} or
 * {@link io.deephaven.engine.table.Table#setLayoutHints(String)}.
 */
@ScriptApi
public class LayoutHintBuilder {
    private static final String AFD_FETCH_PARAM = "fetch";
    private static final int UNDEFINED_FETCH_SIZE = -1;

    private boolean savedLayoutsAllowed = true;
    private Set frontCols;
    private Set backCols;
    private Map autoFilterCols;
    private Set hiddenCols;
    private Set freezeCols;
    private Set alwaysSubscribedCols;
    private Set groupableColumns;

    private Map columnGroups;

    /**
     * The display mode for the search bar.
     */
    public enum SearchDisplayModes {
        /**
         * Use the system default
         */
        Default,
        /**
         * Permit the search bar to be displayed, regardless of system settings
         */
        Show,
        /**
         * Hide the search bar, regardless of system settings.
         */
        Hide
    }

    private SearchDisplayModes searchDisplayMode = SearchDisplayModes.Default;


    /**
     * Helper class to maintain sub-properties for auto filter columns
     */
    private static class AutoFilterData {
        final String column;
        int fetchSize;

        AutoFilterData(String column) {
            this(column, UNDEFINED_FETCH_SIZE);
        }

        AutoFilterData(String column, int fetchSize) {
            this.column = column;
            this.fetchSize = fetchSize;
        }

        /**
         * Serialize this object to a string suitable for inclusion in the builder's parameter string.
         *
         * @return a string of the format column(:param&value)+
         */
        @NotNull
        String serialize() {
            if (fetchSize > 0) {
                return column + ":" + AFD_FETCH_PARAM + "&" + fetchSize;
            }

            return column;
        }

        /**
         * Convert a string of the format defined by {@link #serialize()} into a proper AutoFilterData object
         *
         * @param string the string to parse
         * @return an AutoFilterData instance
         */
        @NotNull
        static AutoFilterData fromString(String string) {
            final String[] parts = string.split(":");
            if (parts.length == 0) {
                throw new IllegalArgumentException("Improperly formatted AutoFilterData string: " + string);
            }

            final String column = parts[0];
            try {
                NameValidator.validateColumnName(column);
            } catch (NameValidator.InvalidNameException ex) {
                throw new IllegalArgumentException("AutoFilterData invalid column name", ex);
            }

            int localFetchSize = UNDEFINED_FETCH_SIZE;
            if (parts.length > 1) {
                for (int i = 1; i < parts.length; i++) {
                    String[] paramParts = parts[i].split("&");
                    if (paramParts.length != 2) {
                        throw new IllegalArgumentException(
                                "Only one value permitted in AutoFilterData parameter string; instead there are: "
                                        + parts.length + " in " + parts[i]);
                    }

                    // noinspection SwitchStatementWithTooFewBranches
                    switch (paramParts[0]) {
                        case AFD_FETCH_PARAM:
                            try {
                                localFetchSize = Integer.parseInt(paramParts[1]);
                            } catch (NumberFormatException ex) {
                                throw new IllegalArgumentException(
                                        "Invalid value for AutoFilterData fetch size parameter: " + paramParts[1]);
                            }
                            break;
                    }
                }
            }

            return new AutoFilterData(column, localFetchSize);
        }
    }

    private static class ColumnGroup {

        private final String name;
        private final List children;
        private final Color color;

        public ColumnGroup(String name, List children, Color color) {
            NameValidator.validateColumnName(name);
            children.forEach(c -> NameValidator.validateColumnName(c));

            this.name = name;
            this.children = children;
            this.color = color;
        }

        @NotNull
        public String serialize() {
            StringBuilder sb = new StringBuilder("name:").append(name);

            sb.append("::children:");
            boolean first = true;
            for (String child : children) {
                if (!first) {
                    sb.append(",");
                }
                first = false;
                sb.append(child);
            }
            if (color != null) {
                sb.append("::color:#")
                        .append(Integer.toHexString(color.javaColor().getRGB()).substring(2));
            }
            return sb.toString();
        }

        /**
         * Convert a string of the format defined by {@link #serialize()} into a proper ColumnGroup object
         *
         * @param string the string to parse
         * @return a ColumnGroup instance
         */
        @NotNull
        public static ColumnGroup fromString(String string) {
            final Map options = Arrays.stream(string.split("::"))
                    .map(option -> option.split(":"))
                    .collect(Collectors.toMap(parts -> parts[0], parts -> parts.length == 2 ? parts[1] : null));

            final String name = options.get("name");
            final List children = Arrays.asList(options.get("children").split(","));
            final String color = options.get("color");

            if (color == null) {
                return new ColumnGroup(name, children, null);
            }

            return new ColumnGroup(name, children, new Color(color));
        }
    }

    private LayoutHintBuilder() {}

    // region Builder Methods

    /**
     * Create a LayoutHintBuilder from the specified parameter string.
     *
     * @param attrs the parameter string
     * @return a LayoutHintBuilder for the input parameter string
     */
    @ScriptApi
    @NotNull
    public static LayoutHintBuilder fromString(String attrs) {
        final Map options = Arrays.stream(attrs.split(";"))
                .map(attr -> attr.split("="))
                .collect(Collectors.toMap(parts -> parts[0], parts -> parts.length == 2 ? parts[1] : ""));

        final LayoutHintBuilder lhb = new LayoutHintBuilder();
        if (options.containsKey("noSavedLayouts")) {
            lhb.savedLayouts(false);
        }

        final String frontStr = options.get("front");
        if (frontStr != null && !frontStr.isEmpty()) {
            lhb.atFront(frontStr.split(","));
        }

        final String endStr = options.get("back");
        if (endStr != null && !endStr.isEmpty()) {
            lhb.atBack(endStr.split(","));
        }

        final String hideStr = options.get("hide");
        if (hideStr != null && !hideStr.isEmpty()) {
            lhb.hide(hideStr.split(","));
        }

        final String autoStr = options.get("autofilter");
        if (!io.deephaven.engine.util.string.StringUtils.isNullOrEmpty(autoStr)) {
            final String[] filters = autoStr.split(",");
            Arrays.stream(filters)
                    .map(AutoFilterData::fromString)
                    .forEach(lhb::addAutofilterData);
        }

        final String freezeStr = options.get("freeze");
        if (freezeStr != null && !freezeStr.isEmpty()) {
            lhb.freeze(freezeStr.split(","));
        }

        final String subscribedStr = options.get("subscribed");
        if (subscribedStr != null && !subscribedStr.isEmpty()) {
            final String[] subscribedColumns = subscribedStr.split(",");
            lhb.alwaysSubscribed(subscribedColumns);
        }

        final String groupableStr = options.get("groupable");
        if (groupableStr != null && !groupableStr.isEmpty()) {
            final String[] groupableColumns = groupableStr.split(",");
            lhb.groupableColumns(groupableColumns);
        }

        final String searchableStr = options.get("searchable");
        if (searchableStr != null && !searchableStr.isEmpty()) {
            lhb.setSearchBarAccess(SearchDisplayModes.valueOf(searchableStr));
        }

        final String columnGroupsStr = options.get("columnGroups");
        if (columnGroupsStr != null && !columnGroupsStr.isEmpty()) {
            Arrays.stream(columnGroupsStr.split("\\|"))
                    .filter(s -> s != null && !s.isEmpty())
                    .map(ColumnGroup::fromString)
                    .forEach(lhb::addColumnGroupData);
        }

        return lhb;
    }

    /**
     * Create a new LayoutHintBuilder.
     *
     * @return a new LayoutHintBuilder
     */
    @ScriptApi
    @NotNull
    public static LayoutHintBuilder get() {
        return new LayoutHintBuilder();
    }

    /**
     * @see LayoutHintBuilder#atFront(Collection)
     */
    @ScriptApi
    public LayoutHintBuilder atFront(String... cols) {
        return atFront(cols == null ? null : Arrays.asList(cols));
    }

    /**
     * Indicate the specified columns should appear as the first N columns of the table when displayed.
     *
     * @param cols the columns to show at front
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder atFront(Collection cols) {
        if (cols == null || cols.isEmpty()) {
            frontCols = null;
            return this;
        }

        if (frontCols == null) {
            frontCols = new LinkedHashSet<>(cols.size());
        }

        frontCols.addAll(cols);

        if (backCols != null) {
            backCols.removeAll(frontCols);
        }

        return this;
    }

    /**
     * @see LayoutHintBuilder#atBack(Collection)
     */
    @ScriptApi
    public LayoutHintBuilder atBack(String... cols) {
        return atBack(cols == null ? null : Arrays.asList(cols));
    }

    /**
     * Indicate the specified columns should appear as the last N columns of the table when displayed.
     *
     * @param cols the columns to show at the back
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder atBack(Collection cols) {
        if (cols == null || cols.isEmpty()) {
            backCols = null;
            return this;
        }

        if (backCols == null) {
            backCols = new LinkedHashSet<>(cols.size());
        }

        backCols.addAll(cols);

        if (frontCols != null) {
            frontCols.removeAll(backCols);
        }

        return this;
    }

    /**
     * @see LayoutHintBuilder#hide(Collection)
     */
    @ScriptApi
    public LayoutHintBuilder hide(String... cols) {
        return hide(cols == null ? null : Arrays.asList(cols));
    }

    /**
     * Indicate the specified columns should be hidden by default.
     *
     * @param cols the columns to initially hide
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder hide(Collection cols) {
        if (cols == null || cols.isEmpty()) {
            hiddenCols = null;
            return this;
        }

        if (hiddenCols == null) {
            hiddenCols = new HashSet<>(cols.size());
        }

        hiddenCols.addAll(cols);

        return this;
    }

    /**
     * @see LayoutHintBuilder#columnGroup(String, List, Color)
     */
    @ScriptApi
    public LayoutHintBuilder columnGroup(String name, List children) {
        return columnGroup(name, children, (Color) null);
    }

    /**
     * @see LayoutHintBuilder#columnGroup(String, List, Color)
     */
    @ScriptApi
    public LayoutHintBuilder columnGroup(String name, List children, String color) {
        if (color == null || color.length() == 0) {
            return columnGroup(name, children, (Color) null);
        }
        return columnGroup(name, children, new Color(color));
    }

    /**
     * Create a named group of columns in the UI
     *
     * @param name the column group name. Must be a valid Deephaven column name
     * @param children the columns and other groups belonging to this group
     * @param color the background color for the group in the UI
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder columnGroup(String name, List children, Color color) {
        if (columnGroups == null) {
            columnGroups = new LinkedHashMap<>();
        }

        if (children.isEmpty()) {
            columnGroups.remove(name);
        } else {
            columnGroups.put(name, new ColumnGroup(name, children, color));
        }

        return this;
    }

    private void addColumnGroupData(ColumnGroup group) {
        if (columnGroups == null) {
            columnGroups = new LinkedHashMap<>();
        }

        columnGroups.put(group.name, group);
    }

    /**
     * @see LayoutHintBuilder#autoFilter(Collection)
     */
    @ScriptApi
    public LayoutHintBuilder autoFilter(String... cols) {
        return autoFilter(cols == null ? null : Arrays.asList(cols));
    }

    /**
     * Indicate the specified columns should be configured as AutoFilter columns
     *
     * @param cols the columns to enable as AutoFilter columns
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder autoFilter(Collection cols) {
        if (cols == null || cols.isEmpty()) {
            autoFilterCols = null;
            return this;
        }

        if (autoFilterCols == null) {
            autoFilterCols = new HashMap<>(cols.size());
        }

        cols.stream()
                .map(AutoFilterData::new)
                .forEach(c -> autoFilterCols.put(c.column, c));

        return this;
    }

    private void addAutofilterData(@NotNull AutoFilterData afd) {
        if (autoFilterCols == null) {
            autoFilterCols = new HashMap<>();
        }

        autoFilterCols.put(afd.column, afd);
    }

    /**
     * Set the default initial number of rows to fetch for columns that have been marked as
     * {@link LayoutHintBuilder#autoFilter(Collection) AutoFilter} columns.
     *
     * @param col the column to set the fetch size for
     * @param size the number of rows to fetch initially
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder autoFilterFetchSize(String col, int size) {
        if (autoFilterCols == null) {
            throw new IllegalStateException("Autofilter is not enabled for any columns");
        }

        final AutoFilterData afd = autoFilterCols.get(col);
        if (afd == null) {
            throw new IllegalArgumentException("Autofilter not enabled for column " + col);
        }

        if (size <= 0) {
            throw new IllegalArgumentException("Illegal Autofilter fetch size: " + size);
        }

        afd.fetchSize = size;

        return this;
    }

    /**
     * @see LayoutHintBuilder#freeze(Collection)
     */
    @ScriptApi
    public LayoutHintBuilder freeze(String... cols) {
        return freeze(cols == null ? null : Arrays.asList(cols));
    }

    /**
     * Indicate the specified columns should be frozen (displayed as the first N, unmovable columns) upon display.
     *
     * @param cols the columns to freeze
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder freeze(Collection cols) {
        if (cols == null || cols.isEmpty()) {
            freezeCols = null;
            return this;
        }

        if (freezeCols == null) {
            freezeCols = new LinkedHashSet<>(cols.size());
        }

        freezeCols.addAll(cols);

        return this;
    }

    /**
     * Indicate that the UI should maintain a subscription to the specified columns within viewports, even if they are
     * out of view.
     *
     * @param columns the columns to keep subscribed
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder alwaysSubscribed(String... columns) {
        if (alwaysSubscribedCols == null) {
            alwaysSubscribedCols = new HashSet<>();
        }

        if (columns != null && columns.length > 0) {
            alwaysSubscribedCols.clear();
            alwaysSubscribedCols.addAll(Arrays.asList(columns));
        }

        return this;
    }

    /**
     * Enable or disable saved layouts for the specified table.
     *
     * @param allowed if layout saving is enabled
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder savedLayouts(boolean allowed) {
        savedLayoutsAllowed = allowed;
        return this;
    }

    /**
     * Set the columns which are allowed to be used as UI-driven rollup columns.
     *
     * @param columns the columns.
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder groupableColumns(String... columns) {
        return groupableColumns(Arrays.asList(columns));
    }

    /**
     * Set the columns which are allowed to be used as UI-driven rollup columns.
     *
     * @param columns the columns.
     * @return this LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder groupableColumns(Collection columns) {
        groupableColumns = new HashSet<>(columns);
        return this;
    }

    /**
     * Set the search bar to explicitly be accessible or inaccessible, or use system default.
     *
     * @param searchable The display mode to use
     * @return This LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder setSearchBarAccess(final SearchDisplayModes searchable) {
        searchDisplayMode = searchable;
        return this;
    }

    /**
     * Set the search bar to explicitly be accessible or inaccessible, or use system default.
     *
     * @param searchable The display mode to use
     * @return This LayoutHintBuilder
     */
    @ScriptApi
    public LayoutHintBuilder setSearchBarAccess(final String searchable) {
        searchDisplayMode = SearchDisplayModes.valueOf(searchable);
        return this;
    }

    // endregion

    /**
     * Create an appropriate parameter string suitable for use with {@link Table#setLayoutHints(String)}.
     *
     * @return this LayoutHintBuilder as a string
     */
    @ScriptApi
    public String build() {
        final StringBuilder sb = new StringBuilder();

        if (!savedLayoutsAllowed) {
            sb.append("noSavedLayouts;");
        }

        if (frontCols != null && !frontCols.isEmpty()) {
            sb.append("front=").append(String.join(",", frontCols)).append(';');
        }

        if (backCols != null && !backCols.isEmpty()) {
            sb.append("back=").append(String.join(",", backCols)).append(';');
        }

        if (hiddenCols != null && !hiddenCols.isEmpty()) {
            sb.append("hide=").append(String.join(",", hiddenCols)).append(';');
        }

        if (autoFilterCols != null && !autoFilterCols.isEmpty()) {
            sb.append("autofilter=").append(
                    autoFilterCols.values().stream().map(AutoFilterData::serialize)
                            .collect(Collectors.joining(",")))
                    .append(';');
        }

        if (freezeCols != null && !freezeCols.isEmpty()) {
            sb.append("freeze=").append(String.join(",", freezeCols)).append(';');
        }

        if (alwaysSubscribedCols != null && !alwaysSubscribedCols.isEmpty()) {
            sb.append("subscribed=").append(String.join(",", alwaysSubscribedCols)).append(';');
        }

        if (groupableColumns != null && !groupableColumns.isEmpty()) {
            sb.append("groupable=").append(String.join(",", groupableColumns)).append(';');
        }

        if (searchDisplayMode != SearchDisplayModes.Default) {
            sb.append("searchable=").append(searchDisplayMode.toString()).append(';');
        }

        if (columnGroups != null && !columnGroups.isEmpty()) {
            sb.append("columnGroups=");
            String groupStrings =
                    columnGroups.values().stream().map(ColumnGroup::serialize).collect(Collectors.joining("|"));
            sb.append(groupStrings).append(';');
        }

        return sb.toString();
    }

    /**
     * Helper method for building and {@link Table#setLayoutHints(String) applying} layout hints to a {@link Table}.
     *
     * @param table The source {@link Table}
     * @return {@code table.setLayoutHints(build())}
     */
    @ScriptApi
    public Table applyToTable(@NotNull final Table table) {
        return table.setLayoutHints(build());
    }

    // region Getters

    /**
     * Check if saved layouts should be allowed.
     *
     * @return if saved layouts are allowed
     */
    public boolean areSavedLayoutsAllowed() {
        return savedLayoutsAllowed;
    }

    /**
     * Get the ordered set of columns that should be displayed up front.
     *
     * @return an ordered set of columns to display up as the first N columns
     */
    public @NotNull Set getFrontCols() {
        return frontCols == null ? Collections.emptySet() : Collections.unmodifiableSet(frontCols);
    }

    /**
     * Get the ordered set of columns that should be displayed as the last N columns.
     *
     * @return an ordered set of columns to display at the end.
     */
    public @NotNull Set getBackCols() {
        return backCols == null ? Collections.emptySet() : Collections.unmodifiableSet(backCols);
    }

    /**
     * Get the set of columns that should be hidden by default.
     *
     * @return the set of columns that should be hidden
     */
    public @NotNull Set getHiddenCols() {
        return hiddenCols == null ? Collections.emptySet() : Collections.unmodifiableSet(hiddenCols);
    }

    /**
     * Get the set of columns that should be enabled for AutoFilter.
     *
     * @return the set of columns enabled for AutoFilter
     */
    public @NotNull Set getAutoFilterCols() {
        return autoFilterCols == null ? Collections.emptySet() : Collections.unmodifiableSet(autoFilterCols.keySet());
    }

    /**
     * Get the number of rows to fetch in the initial AutoFilter data fetch.
     *
     * @param column the column to set the fetch size for
     * @return the number of rows to fetch initially
     */
    public int getAutoFilterFetchSize(String column) {
        if (autoFilterCols == null) {
            return UNDEFINED_FETCH_SIZE;
        }

        final AutoFilterData afd = autoFilterCols.get(column);
        return afd == null ? UNDEFINED_FETCH_SIZE : afd.fetchSize;
    }

    /**
     * Get the ordered set of columns that should be frozen.
     *
     * @return the ordered set of columns that should be frozen
     */
    public @NotNull Set getFreezeCols() {
        return freezeCols == null ? Collections.emptySet() : Collections.unmodifiableSet(freezeCols);
    }

    /**
     * Get the set of columns that should always remain subscribed.
     *
     * @return the set of columns to be subscribed.
     */
    public @NotNull Set getAlwaysSubscribedCols() {
        return alwaysSubscribedCols == null ? Collections.emptySet()
                : Collections.unmodifiableSet(alwaysSubscribedCols);
    }

    /**
     * Get the set of columns allowed to participate in UI-driven rollups.
     *
     * @return the set of columns
     */
    public @NotNull Set getGroupableColumns() {
        return groupableColumns == null ? Collections.emptySet() : Collections.unmodifiableSet(groupableColumns);
    }

    public @NotNull SearchDisplayModes getSearchDisplayMode() {
        if (searchDisplayMode == null) {
            searchDisplayMode = SearchDisplayModes.Default;
        }
        return searchDisplayMode;
    }

    /**
     * Get the map of column groups for the UI.
     *
     * @return the map of column groups
     */
    public @NotNull Map getColumnGroups() {
        return columnGroups == null ? Collections.emptyMap() : Collections.unmodifiableMap(columnGroups);
    }

    /**
     * Get the column group for the specified name.
     *
     * @param name the name of the column group
     * @return the column group if it exists
     */
    public @NotNull ColumnGroup getColumnGroup(String name) {
        return columnGroups == null ? null : columnGroups.get(name);
    }
    // endregion
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy