001/*****************************************************************************
002 * Copyright by The HDF Group.                                               *
003 * Copyright by the Board of Trustees of the University of Illinois.         *
004 * All rights reserved.                                                      *
005 *                                                                           *
006 * This file is part of the HDF Java Products distribution.                  *
007 * The full copyright notice, including terms governing use, modification,   *
008 * and redistribution, is contained in the files COPYING and Copyright.html. *
009 * COPYING can be found at the root of the source code distribution tree.    *
010 * Or, see https://support.hdfgroup.org/products/licenses.html               *
011 * If you do not have access to either file, you may request a copy from     *
012 * help@hdfgroup.org.                                                        *
013 ****************************************************************************/
014
015package hdf.view.TableView;
016
017import java.lang.reflect.Array;
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.ListIterator;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.eclipse.nebula.widgets.nattable.NatTable;
028import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
029import org.eclipse.nebula.widgets.nattable.config.EditableRule;
030import org.eclipse.nebula.widgets.nattable.config.IEditableRule;
031import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
032import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
033import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
034import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
035import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
036import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer;
037import org.eclipse.nebula.widgets.nattable.group.ColumnGroupExpandCollapseLayer;
038import org.eclipse.nebula.widgets.nattable.group.ColumnGroupGroupHeaderLayer;
039import org.eclipse.nebula.widgets.nattable.group.ColumnGroupHeaderLayer;
040import org.eclipse.nebula.widgets.nattable.group.ColumnGroupModel;
041import org.eclipse.nebula.widgets.nattable.group.ColumnGroupModel.ColumnGroup;
042import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
043import org.eclipse.nebula.widgets.nattable.layer.ILayer;
044import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
045import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
046import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
047import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
048import org.eclipse.nebula.widgets.nattable.selection.event.CellSelectionEvent;
049import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
050import org.eclipse.swt.SWT;
051import org.eclipse.swt.custom.ScrolledComposite;
052import org.eclipse.swt.widgets.Composite;
053
054import hdf.object.CompoundDS;
055import hdf.object.CompoundDataFormat;
056import hdf.object.DataFormat;
057import hdf.object.Datatype;
058import hdf.view.Tools;
059import hdf.view.ViewProperties;
060import hdf.view.DataView.DataViewManager;
061
062/**
063 * A class to construct a CompoundDS TableView.
064 */
065public class DefaultCompoundDSTableView extends DefaultBaseTableView implements TableView {
066
067    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultCompoundDSTableView.class);
068
069    /**
070     * Constructs a CompoundDS TableView with no additional data properties.
071     *
072     * @param theView
073     *            the main HDFView.
074     */
075    public DefaultCompoundDSTableView(DataViewManager theView) {
076        this(theView, null);
077    }
078
079    /**
080     * Constructs a CompoundDS TableView with the specified data properties.
081     *
082     * @param theView
083     *            the main HDFView.
084     *
085     * @param dataPropertiesMap
086     *            the properties on how to show the data. The map is used to allow
087     *            applications to pass properties on how to display the data, such
088     *            as: transposing data, showing data as characters, applying a
089     *            bitmask, and etc. Predefined keys are listed at
090     *            ViewProperties.DATA_VIEW_KEY.
091     */
092    @SuppressWarnings("rawtypes")
093    public DefaultCompoundDSTableView(DataViewManager theView, HashMap dataPropertiesMap) {
094        super(theView, dataPropertiesMap);
095
096        isDataTransposed = false; // Disable transpose for compound datasets
097
098        if (!shell.isDisposed()) {
099            shell.setImage(ViewProperties.getTableIcon());
100
101            viewer.addDataView(this);
102
103            shell.open();
104        }
105    }
106
107    @Override
108    protected void loadData(DataFormat dataObject) throws Exception {
109        super.loadData(dataObject);
110
111        if (dataValue == null) {
112            log.debug("loadData(): data value is null");
113            throw new RuntimeException("data value is null or not a list");
114        }
115    }
116
117    /**
118     * Creates a NatTable for a Compound dataset
119     *
120     * @param parent
121     *            The parent for the NatTable
122     * @param dataObject
123     *            The Compound dataset for the NatTable to display
124     *
125     * @return The newly created NatTable
126     */
127    @Override
128    protected NatTable createTable(Composite parent, DataFormat dataObject) {
129        // Create body layer
130        final ColumnGroupModel columnGroupModel = new ColumnGroupModel();
131        final ColumnGroupModel secondLevelGroupModel = new ColumnGroupModel();
132
133        try {
134            dataProvider = DataProviderFactory.getDataProvider(dataObject, dataValue, isDataTransposed);
135
136            log.trace("createTable(): rows={} : cols={}", dataProvider.getRowCount(), dataProvider.getColumnCount());
137
138            dataLayer = new DataLayer(dataProvider);
139        }
140        catch (Exception ex) {
141            log.debug("createTable(): failed to retrieve DataProvider for table: ", ex);
142            return null;
143        }
144
145        final ColumnGroupExpandCollapseLayer expandCollapseLayer = new ColumnGroupExpandCollapseLayer(dataLayer,
146                secondLevelGroupModel, columnGroupModel);
147        selectionLayer = new SelectionLayer(expandCollapseLayer);
148        final ViewportLayer viewportLayer = new ViewportLayer(selectionLayer);
149
150        dataLayer.setDefaultColumnWidth(80);
151
152        // Create the Column Header layer
153        columnHeaderDataProvider = new CompoundDSColumnHeaderDataProvider(dataObject);
154        ColumnHeaderLayer columnHeaderLayer = new ColumnHeader(new DataLayer(columnHeaderDataProvider), viewportLayer,
155                selectionLayer);
156
157        // Set up column grouping
158        ColumnGroupHeaderLayer columnGroupHeaderLayer = new ColumnGroupHeaderLayer(columnHeaderLayer, selectionLayer,
159                columnGroupModel);
160        CompoundDSNestedColumnHeaderLayer nestedColumnGroupHeaderLayer = new CompoundDSNestedColumnHeaderLayer(
161                columnGroupHeaderLayer, selectionLayer, secondLevelGroupModel);
162
163        // Create the Row Header layer
164        rowHeaderDataProvider = new RowHeaderDataProvider(dataObject);
165
166        // Try to adapt row height to current font
167        int defaultRowHeight = curFont == null ? 20 : (2 * curFont.getFontData()[0].getHeight());
168
169        DataLayer baseLayer = new DataLayer(rowHeaderDataProvider, 40, defaultRowHeight);
170        RowHeaderLayer rowHeaderLayer = new RowHeader(baseLayer, viewportLayer, selectionLayer);
171
172        // Create the Corner Layer
173        ILayer cornerLayer = new CornerLayer(
174                new DataLayer(new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider)),
175                rowHeaderLayer, nestedColumnGroupHeaderLayer);
176
177        // Create the Grid Layer
178        GridLayer gridLayer = new EditingGridLayer(viewportLayer, nestedColumnGroupHeaderLayer, rowHeaderLayer,
179                cornerLayer);
180
181        final NatTable natTable = new NatTable(parent, gridLayer, false);
182        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
183        natTable.addLayerListener(new CompoundDSCellSelectionListener());
184
185        // Create popup menu for region or object ref.
186        //if (isStdRef || isRegRef || isObjRef) {
187        //    natTable.addConfiguration(new RefContextMenu(natTable));
188        //}
189
190        natTable.configure();
191
192        return natTable;
193    }
194
195    @Override
196    public Object getSelectedData() {
197        Object selectedData = null;
198
199        int cols = this.getSelectedColumnCount();
200        int rows = this.getSelectedRowCount();
201
202        if ((cols <= 0) || (rows <= 0)) {
203            shell.getDisplay().beep();
204            Tools.showError(shell, "Select", "No data is selected.");
205            return null;
206        }
207
208        Object colData = null;
209        try {
210            colData = ((List<?>) dataObject.getData()).get(selectionLayer.getSelectedColumnPositions()[0]);
211        }
212        catch (Exception ex) {
213            log.debug("getSelectedData(): ", ex);
214            return null;
215        }
216
217        int size = Array.getLength(colData);
218        String cName = colData.getClass().getName();
219        int cIndex = cName.lastIndexOf('[');
220        char nt = ' ';
221        if (cIndex >= 0) {
222            nt = cName.charAt(cIndex + 1);
223        }
224        log.trace("getSelectedData(): size={} cName={} nt={}", size, cName, nt);
225
226        if (isStdRef) {
227            // std. ref data are stored in bytess
228            selectedData = new byte[size];
229        }
230        else if (isRegRef) {
231            // reg. ref data are stored in strings
232            selectedData = new String[size];
233        }
234        else {
235            switch (nt) {
236                case 'B':
237                    selectedData = new byte[size];
238                    break;
239                case 'S':
240                    selectedData = new short[size];
241                    break;
242                case 'I':
243                    selectedData = new int[size];
244                    break;
245                case 'J':
246                    selectedData = new long[size];
247                    break;
248                case 'F':
249                    selectedData = new float[size];
250                    break;
251                case 'D':
252                    selectedData = new double[size];
253                    break;
254                default:
255                    selectedData = null;
256                    break;
257            }
258        }
259
260        if (selectedData == null) {
261            shell.getDisplay().beep();
262            Tools.showError(shell, "Select", "Unsupported data type.");
263            return null;
264        }
265
266        log.trace("getSelectedData(): selectedData is type {}", nt);
267
268        System.arraycopy(colData, 0, selectedData, 0, size);
269
270        return selectedData;
271    }
272
273
274    @Override
275    protected void showObjRefData(long[] ref) {
276        // Currently no support for showing Obj. Ref. Data in Compound Datasets
277    }
278
279    @Override
280    protected void showRegRefData(String reg) {
281        // Currently no support for show Reg. Ref. Data in Compound Datasets
282    }
283
284    @Override
285    protected void showStdRefData(byte[] reg) {
286        // Currently no support for show Std. Ref. Data in Compound Datasets
287    }
288
289    /**
290     * Returns an IEditableRule that determines whether cells can be edited.
291     *
292     * Cells can be edited as long as the dataset is not opened in read-only mode
293     * and the data is not currently displayed in hexadecimal, binary, or character
294     * mode.
295     *
296     * @param dataObject
297     *            The dataset for editing
298     *
299     * @return a new IEditableRule for the dataset
300     */
301    @Override
302    protected IEditableRule getDataEditingRule(DataFormat dataObject) {
303        if (dataObject == null) return null;
304
305        // Only Allow editing if not in read-only mode
306        return new EditableRule() {
307            @Override
308            public boolean isEditable(int columnIndex, int rowIndex) {
309                /*
310                 * TODO: Should be able to edit character-displayed types and datasets when
311                 * displayed as hex/binary.
312                 */
313                //return !(isReadOnly || isDisplayTypeChar || showAsBin || showAsHex);
314                return !isReadOnly;
315            }
316        };
317    }
318
319    /**
320     * Update cell value label and cell value field when a cell is selected
321     */
322    private class CompoundDSCellSelectionListener implements ILayerListener {
323        @Override
324        public void handleLayerEvent(ILayerEvent e) {
325            if (e instanceof CellSelectionEvent) {
326                log.trace("ScalarDSCellSelectionListener: CellSelected isStdRef={} isRegRef={} isObjRef={}", isStdRef, isRegRef, isObjRef);
327
328                CellSelectionEvent event = (CellSelectionEvent) e;
329                Object val = dataTable.getDataValueByPosition(event.getColumnPosition(), event.getRowPosition());
330
331                int rowStart = ((RowHeaderDataProvider) rowHeaderDataProvider).start;
332                int rowStride = ((RowHeaderDataProvider) rowHeaderDataProvider).stride;
333
334                int rowIndex = rowStart + indexBase + dataTable.getRowIndexByPosition(event.getRowPosition()) * rowStride;
335                Object fieldName = columnHeaderDataProvider.getDataValue(dataTable.getColumnIndexByPosition(event.getColumnPosition()), 0);
336
337                String colIndex = "";
338                if (dataObject.getWidth() > 1) {
339                    int groupSize = ((CompoundDataFormat) dataObject).getSelectedMemberCount();
340                    colIndex = "[" + String.valueOf((dataTable.getColumnIndexByPosition(event.getColumnPosition())) / groupSize) + "]";
341                }
342
343                cellLabel.setText(String.valueOf(rowIndex) + ", " + fieldName + colIndex + " =  ");
344
345                if (val == null) {
346                    cellValueField.setText("Null");
347                    return;
348                }
349
350                ILayerCell cell = dataTable.getCellByPosition(((CellSelectionEvent) e).getColumnPosition(), ((CellSelectionEvent) e).getRowPosition());
351                cellValueField.setText(dataDisplayConverter.canonicalToDisplayValue(cell, dataTable.getConfigRegistry(), val).toString());
352                ((ScrolledComposite) cellValueField.getParent()).setMinSize(cellValueField.computeSize(SWT.DEFAULT, SWT.DEFAULT));
353            }
354        }
355    }
356
357    /**
358     * Custom Column Header data provider to set column names based on selected
359     * members for Compound Datasets.
360     */
361    private class CompoundDSColumnHeaderDataProvider implements IDataProvider {
362        // Column names with CompoundDS SEPARATOR character '->' left intact.
363        // Used in CompoundDSNestedColumnHeader to provide correct nesting structure.
364        private final String[]          columnNamesFull;
365
366        // Simplified base column names without separator character. Used to
367        // actually label the columns.
368        private final ArrayList<String> columnNames;
369
370        private final int               ncols;
371        private final int               groupSize;
372
373        public CompoundDSColumnHeaderDataProvider(DataFormat dataObject) {
374            CompoundDataFormat dataFormat = (CompoundDataFormat) dataObject;
375
376            Datatype cmpdType = dataObject.getDatatype();
377            List<Datatype> selectedTypes = DataFactoryUtils.filterNonSelectedMembers(dataFormat, cmpdType);
378            final List<String> datasetMemberNames = Arrays.asList(dataFormat.getSelectedMemberNames());
379
380            columnNames = new ArrayList<>(dataFormat.getSelectedMemberCount());
381
382            recursiveColumnHeaderSetup(columnNames, dataFormat, cmpdType, datasetMemberNames, selectedTypes);
383
384            // Make a copy of column names so changes to column names don't affect the full column names,
385            // which is used elsewhere.
386            columnNamesFull = Arrays.copyOf(columnNames.toArray(new String[0]), columnNames.size());
387
388            // Simplify any nested field column names down to their base names. E.g., a
389            // nested field with the full name 'nested_name->a_name' has a simplified column
390            // name of 'a_name'
391            for (int j = 0; j < columnNames.size(); j++) {
392                String nestedName = columnNames.get(j);
393                int nestingPosition = nestedName.lastIndexOf("->");
394
395                // If this is a nested field, this column's name is whatever follows the last
396                // nesting character '->'
397                if (nestingPosition >= 0)
398                    columnNames.set(j, nestedName.substring(nestingPosition + 2));
399            }
400
401            groupSize = columnNames.size();
402
403            ncols = columnNames.size() * (int) dataFormat.getWidth();
404            log.trace("CompoundDSColumnHeaderDataProvider: ncols={}", ncols);
405        }
406
407        private void recursiveColumnHeaderSetup(List<String> outColNames, CompoundDataFormat dataFormat,
408                Datatype curDtype, List<String> memberNames, List<Datatype> memberTypes) {
409
410            if (curDtype.isArray()) {
411                /*
412                 * ARRAY of COMPOUND type
413                 */
414                int arrSize = 1;
415                Datatype nestedCompoundType = curDtype;
416                while (nestedCompoundType != null) {
417                    if (nestedCompoundType.isCompound()) {
418                        break;
419                    }
420                    else if (nestedCompoundType.isArray()) {
421                        long[] arrayDims = nestedCompoundType.getArrayDims();
422                        for (int i = 0; i < arrayDims.length; i++) {
423                            arrSize *= arrayDims[i];
424                        }
425                    }
426
427                    nestedCompoundType = nestedCompoundType.getDatatypeBase();
428                }
429
430                log.trace("recursiveColumnHeaderSetup(): ARRAY size: {}", arrSize);
431
432                /*
433                 * TODO: Temporary workaround for top-level array of compound types.
434                 */
435                if (memberTypes.isEmpty()) {
436                    memberTypes = DataFactoryUtils.filterNonSelectedMembers(dataFormat, nestedCompoundType);
437                }
438
439                /*
440                 * Duplicate member names by the size of the array.
441                 *
442                 * NOTE: this assumes that the member names of the ARRAY/VLEN of COMPOUND
443                 * directly follow the name of the top-level member itself and will break if
444                 * that assumption is not true.
445                 */
446                StringBuilder sBuilder = new StringBuilder();
447                ArrayList<String> nestedMemberNames = new ArrayList<>(arrSize * memberNames.size());
448                for (int i = 0; i < arrSize; i++) {
449                    for (int j = 0; j < memberNames.size(); j++) {
450                        sBuilder.setLength(0);
451
452                        // Copy the dataset member name reference, so changes to the column name
453                        // don't affect the dataset's internal member names.
454                        sBuilder.append(memberNames.get(j).replaceAll(CompoundDS.SEPARATOR, "->"));
455
456                        /*
457                         * Add the index number to the member name so we can correctly setup nested
458                         * column grouping.
459                         */
460                        sBuilder.append("[" + i + "]");
461
462                        nestedMemberNames.add(sBuilder.toString());
463                    }
464                }
465
466                recursiveColumnHeaderSetup(outColNames, dataFormat, nestedCompoundType, nestedMemberNames, memberTypes);
467            }
468            else if (curDtype.isVLEN() && !curDtype.isVarStr()) {
469                /*
470                 * TODO: empty until we have true variable-length support.
471                 */
472            }
473            else if (curDtype.isCompound()) {
474                ListIterator<String> localIt = memberNames.listIterator();
475                while (localIt.hasNext()) {
476                    int curIdx = localIt.nextIndex();
477                    String curName = localIt.next();
478                    Datatype curType = memberTypes.get(curIdx % memberTypes.size());
479                    Datatype nestedArrayOfCompoundType = null;
480                    boolean nestedArrayOfCompound = false;
481
482                    /*
483                     * Recursively detect any nested array/vlen of compound types and deal with them
484                     * by creating multiple copies of the member names.
485                     */
486                    if (curType.isArray() /* || (curType.isVLEN() && !curType.isVarStr()) */ /* TODO: true variable-length support */) {
487                        Datatype base = curType.getDatatypeBase();
488                        while (base != null) {
489                            if (base.isCompound()) {
490                                nestedArrayOfCompound = true;
491                                nestedArrayOfCompoundType = base;
492                                break;
493                            }
494
495                            base = base.getDatatypeBase();
496                        }
497                    }
498
499                    /*
500                     * For ARRAY of COMPOUND and VLEN of COMPOUND types, we repeat the compound
501                     * members n times, where n is the number of array or vlen elements.
502                     */
503                    if (nestedArrayOfCompound) {
504                        List<Datatype> selTypes = DataFactoryUtils.filterNonSelectedMembers(dataFormat, nestedArrayOfCompoundType);
505                        List<String> selMemberNames = new ArrayList<>(selTypes.size());
506
507                        int arrCmpdLen = calcArrayOfCompoundLen(selTypes);
508                        for (int i = 0; i < arrCmpdLen; i++) {
509                            selMemberNames.add(localIt.next());
510                        }
511
512                        recursiveColumnHeaderSetup(outColNames, dataFormat, curType, selMemberNames, selTypes);
513                    }
514                    else {
515                        // Copy the dataset member name reference, so changes to the column name
516                        // don't affect the dataset's internal member names.
517                        curName = new String(curName.replaceAll(CompoundDS.SEPARATOR, "->"));
518
519                        outColNames.add(curName);
520                    }
521                }
522            }
523        }
524
525        private int calcArrayOfCompoundLen(List<Datatype> datatypes) {
526            int count = 0;
527            Iterator<Datatype> localIt = datatypes.iterator();
528            while (localIt.hasNext()) {
529                Datatype curType = localIt.next();
530
531                if (curType.isCompound()) {
532                    count += calcArrayOfCompoundLen(curType.getCompoundMemberTypes());
533                }
534                else if (curType.isArray()) {
535                    /*
536                     * TODO: nested array of compound length calculation
537                     */
538                }
539                else
540                    count++;
541            }
542
543            return count;
544        }
545
546        @Override
547        public int getColumnCount() {
548            return ncols;
549        }
550
551        @Override
552        public int getRowCount() {
553            return 1;
554        }
555
556        @Override
557        public Object getDataValue(int columnIndex, int rowIndex) {
558            try {
559                return columnNames.get(columnIndex % groupSize);
560            }
561            catch (Exception ex) {
562                log.debug("getDataValue({}, {}): ", rowIndex, columnIndex, ex);
563                return "*ERROR*";
564            }
565        }
566
567        @Override
568        public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
569            // Disable column header editing
570            return;
571        }
572    }
573
574    /**
575     * Implementation of Column Grouping for Compound Datasets with nested members.
576     */
577    private class CompoundDSNestedColumnHeaderLayer extends ColumnGroupGroupHeaderLayer {
578        public CompoundDSNestedColumnHeaderLayer(ColumnGroupHeaderLayer columnGroupHeaderLayer,
579                SelectionLayer selectionLayer, ColumnGroupModel columnGroupModel) {
580            super(columnGroupHeaderLayer, selectionLayer, columnGroupModel);
581
582            if (curFont != null) {
583                this.setRowHeight(2 * curFont.getFontData()[0].getHeight());
584                columnGroupHeaderLayer.setRowHeight(2 * curFont.getFontData()[0].getHeight());
585            }
586
587            final String[] allColumnNames = ((CompoundDSColumnHeaderDataProvider) columnHeaderDataProvider).columnNamesFull;
588            final int groupSize = ((CompoundDSColumnHeaderDataProvider) columnHeaderDataProvider).groupSize;
589            log.trace("CompoundDSNestedColumnHeaderLayer: groupSize={} -- allColumnNames={}", groupSize, allColumnNames);
590
591            // Set up first-level column grouping
592            int[] indices = new int[groupSize];
593            for (int i = 0; i < dataObject.getWidth(); i++) {
594                for (int j = 0; j < groupSize; j++) {
595                    indices[j] = (i * groupSize) + j;
596                }
597
598                this.addColumnsIndexesToGroup(String.valueOf(i), indices);
599            }
600
601            // Set up any further-nested column groups
602            StringBuilder columnHeaderBuilder = new StringBuilder();
603            for (int k = 0; k < dataObject.getWidth(); k++) {
604                for (int i = 0; i < allColumnNames.length; i++) {
605                    int colindex = i + k * allColumnNames.length;
606                    int nestingPosition = allColumnNames[i].lastIndexOf("->");
607
608                    columnHeaderBuilder.setLength(0);
609
610                    if (nestingPosition >= 0) {
611                        ColumnGroup nestingGroup = columnGroupModel.getColumnGroupByIndex(colindex);
612                        if (nestingGroup != null) {
613                            String columnGroupName = nestingGroup.getName();
614                            int groupTitleStartPosition = allColumnNames[i].lastIndexOf("->", nestingPosition);
615                            String nestingName = allColumnNames[i].substring(nestingPosition + 2);
616                            String newGroupName;
617
618                            if (groupTitleStartPosition == 0) {
619                                /* Singly nested member */
620                                newGroupName = allColumnNames[i].substring(groupTitleStartPosition, nestingPosition);
621                            }
622                            else if (groupTitleStartPosition > 0) {
623                                /* Member nested at second level or beyond, skip past leading '->' */
624                                newGroupName = allColumnNames[i].substring(0, groupTitleStartPosition);
625                            }
626                            else {
627                                newGroupName = allColumnNames[i].substring(0, nestingPosition);
628                            }
629
630                            columnHeaderBuilder.append(newGroupName);
631                            columnHeaderBuilder.append("{").append(columnGroupName).append("}");
632
633                            /*
634                             * Special case for ARRAY of COMPOUND and VLEN of COMPOUND types.
635                             *
636                             * NOTE: This is a quick and dirty way of determining array/vlen of compound
637                             * members. It will probably cause weird column grouping behavior if a user uses
638                             * the "[number]" pattern in one of their member names, but for now we won't
639                             * worry about it.
640                             */
641                            if (nestingName.matches(".*\\[[0-9]*\\]")) {
642                                processArrayOfCompound(columnHeaderBuilder, nestingName);
643                            }
644
645                            columnGroupHeaderLayer.addColumnsIndexesToGroup(columnHeaderBuilder.toString(), colindex);
646                        }
647                        else
648                            log.debug("CompoundDSNestedColumnHeaderLayer: nesting group was null for index {}", colindex);
649                    }
650                    else if (allColumnNames[i].matches(".*\\[[0-9]*\\]")) {
651                        /*
652                         * Top-level ARRAY of COMPOUND types.
653                         */
654                        columnHeaderBuilder.append("ARRAY");
655                        processArrayOfCompound(columnHeaderBuilder, allColumnNames[i]);
656
657                        columnGroupHeaderLayer.addColumnsIndexesToGroup(columnHeaderBuilder.toString(), colindex);
658                    }
659                }
660            }
661        }
662
663        private void processArrayOfCompound(StringBuilder curBuilder, String columnName) {
664            Pattern indexPattern = Pattern.compile(".*\\[([0-9]*)\\]");
665            Matcher indexMatcher = indexPattern.matcher(columnName);
666
667            /*
668             * Group array/vlen of compounds members into array-indexed groups.
669             */
670            if (indexMatcher.matches()) {
671                int containerIndex = 0;
672
673                try {
674                    containerIndex = Integer.parseInt(indexMatcher.group(1));
675                }
676                catch (Exception ex) {
677                    log.debug("processArrayOfCompound(): error parsing array/vlen of compound index: ", ex);
678                    return;
679                }
680
681                curBuilder.append("[").append(containerIndex).append("]");
682            }
683        }
684    }
685}