An overview of core classes, concepts, and structure.
This is NOT a complete and detailed description, you will need to refer to javadoc and source code for the actual and current details.
You can use the IntelliJ feature to view a class diagram of the classes; in the project tool window, right-click the json package and select Diagrams->Show Diagram (filter on Open Files for a decent overview):
BsonDocumentDataSet
is the data set we get from the driverBsonDocumentViewer
is used for the data tab in the Object ViewerBsonDocumentResultSetViewer
shows data produced by the SQL Commander.JsonDockPanel
is the GUI container for the various views and instantiated by the viewers, responsible for initializing and connects the
views.
A CardPanel
holds the main view panels, an internal DockFrame
wraps the secondary panels (nested details and editor), thus
making them dockable frames that can be hidden or shown depending on the active panel. The location of the panels are fixed, by the relative size can
be adjusted using splitters.
AbstractAdaptableTreeTableModel.load()
is where subclasses convert the dataset into a model for their purpose.DataSetCellFormPanel.setSourceDataSet(...)
and JsonDataSetCellFormPanel.showValue(...)
are used to load the text panelTreeTableModelAdapter
is implemented to let models parse data from various sources; Jackson and Mongo handle JSON structures differently,
not only by using different types but also by slightly different structures at the root level.
AbstractAdaptableTreeTableModel
is an abstraction of models that can be adapted to the specific data using a TreeTableModelAdapter
.
NestedTreeTableModel
is used for the nested view.AdaptableTreeTableModel
is used for the tree view.NestedDetailsTreeTableModel
extends AdaptableTreeTableModel
and is used for the details tree in the nested view.AbstractDataSetPanel
is the superclass of the model-based viewsexecutionWillStart()
clears the view before new data is loadedexecutionFinished()
converts the data set into models for the various viewsNestedTreeTableGridPanel
implements the nested view using a NestedTreeTableModel
and a NestedColumnManager
NestedDetailsTreeTableGridPanel
is a tree view showing the details of a cell in the nested view using a
NestedDetailsTreeTableModel
.
JsonDataSetCellFormPanel
shows the data value of a cell selected in the details tree.
It extends the DataSetCellFormPanel
, adapted for running as a panel rather than a dialog.
AdaptableTreeTableGridPanel
implements the tree view using an AdaptableTreeTableModel
.
It is essentially the same view as the details panel in the nested view, but running on the full dataset instead of a single cell.
It uses its own instance of the JsonDataSetCellFormPanel
to show the data value of a cell selected in the tree.
AdaptableTreeTableTextPanel
extends DataSetTextPanel
and shows the entire dataset in textual form. It can format/unformat JSON
structures and can be extended to handle JSON syntax.
AbstractAdaptableTreeTableGridPanel.setDataSet(...)
is the entry point for setting up and connecting models and panelsAbstractAdaptableTreeTableGridPanel.connectTreeAndEditor(...)
uses an internal listener to listen to cell selection and tree expansion events and load data into the text panelNestedTreeTableGridPanel.initializeNestedModel(...)
uses an internal listener to listen for cell selections and call NestedDetailsTreeTableGridPanel.load(...)
to show data in the details treeNestedTableHeader
and
TableColumnGroup
classes to render columns in nested groups that can be expanded or collapsed.
NestedTreeTableModel
is an AbstractAdaptableTreeTableModel
that converts the dataset into a two-dimensional model were rows cannot be expanded or collapsed.NestedColumnManager
is a helper class for the model. It uses internal sets and maps of identifiers to keep track of header columns and groups.The nested view holds all columns as regular columns, but also one extra column for each column group, the group column . In the expanded state, the group column is hidden. In the collapsed state, the group column is made visible and all other columns in the column group are hidden.
The group column is marked to stand out; at the time of writing, it is surrounded with curly brackets.
Example:
A group named address
would have a group column called {address}
.
The cell value of a collapsed group (i.e. the group column) is rendered as the number of elements (columns) the group holds.
Example:
A collapsed group with three hidden columns (or column groups) would be rendered as {3}
.
The Group Column Identifier
Since element must have a unique name within is container in the JSON structure, and since the NestedColumnManager
keeps track of the identifiers
of all group columns, we should be able to name the group column after the object it represents in the collapsed state and treat the curly brackets as visual decorators.
However, since all columns are identified by name and the TableColumnGroup
extends TableColumn
, it gets a bit complicated to identify
and distinguish the group from the group column. A lot of the grid-related code depend on unique identifiers for converting between columns and column indexes;
we need a mechanism to map a column group to a group column and vice versa.
Wrapping a column group name in curly brackets makes it a group column name, and stripping the curly brackets from a group column name makes it a column group name.
Caveat: Although all column identifiers are tracked by the NestedColumnManager
, this is not a foolproof approach. See Quirks below,
Collections or arrays cannot be easily represented in the nested view since it would require that we make the individual columns vertically expandable. Besides the coding challenge, there is also a problem with the visual representation. In the key-value based tree view, this is simple, but what do you show in other columns when you expand one column in a grid view?
For this reason, we render the cell value of iterables as the number of elements (example: [42]
) and expand the cell in the details tree when the user clicks the cell.
Values that we cannot easily calculate the size of (like nested arrays) are rendered without the size (example: [...]
).
In contrast to a regular relational table, a JSON document may have several fields with the same name, as long as they belong to different column groups.
To handle this, we use identifiers to uniquely name fields and columns, thus giving them qualified names, much like a java class.
Most table classes, both our own and the JIDE and Swing superclasses, already support the notion of identifiers and can transparently handle names or
identifiers if (and this is important) the model implements ColumnIdentifierTableModel
.
Since JSON accepts more or less any UNICODE character as a valid name, we construct the identifiers using NUL
(#0000
) as delimiter.
Example:
{ "user": { "name": "Carl", "contact": { "address": { "street": "First Street 1", "zip": 123456, "city": "The Capital" }, "phone": { "home": 1234567, "work": 2345678 } } } }The resulting columns, including the group columns that are shown only when the group is collapsed, the names and identifiers of the resulting columns would be (in column order):
user: {user} name: user[NUL]name contact: user[NUL]{contact} address: user[NUL]contact[NUL]{address} street: user[NUL]contact[NUL]address[NUL]street zip: user[NUL]contact[NUL]address[NUL]zip city: user[NUL]contact[NUL]address[NUL]city phone: user[NUL]contact[NUL]{phone} home: user[NUL]contact[NUL]phone[NUL]home work: user[NUL]contact[NUL]phone[NUL]work
A consequence of reading document data without a schema is that you don't know beforehand what columns you will get. In the trivial case, this is just a nuisance where fields that belong together show up in random order.
Example:
Row 1: city phone Row 2: street city phone --- Expect: street city phone Actual: city phone streetThis may be OK, but when grouping columns, the problem gets worse:
Row 1: address/city phone Row 2: address/street address/city phone --- Expect: address/street address/city phone Actual: address/city phone address/streetThis becomes a GUI problem since the grouped columns
city
and street
are supposed to be contained in the group address
:
| 0 | 1 | 2 | ----------------------------- | address | | address | | city | phone | street |Hence we assemble the
address
group by moving address/street
from column 2
to 1
and inserting it before phone
,
which consequently is shuffled from column 1
to 2
:
| 0 | 1 | 2 | ------------------------- | address | | | city | street | phone |
To reorganize the columns and assemble each group to span its contained columns in consecutive order, the column manager analyzes the order and shuffles the
columns after loading the model (when we have all columns) but before we load the grid (when we create the TableColumnGroup
s).
The approach is pretty simple; since the column manager knows the index of all columns that belong to each group, we create a map that shows how to map each
column to the desired place and then call DataSet.moveColumn()
to shuffle the columns just like a user does in the GUI.
In pseudocode:
assemble(columns): list = mapColumns(columns) moveColumns(list) mapColumns(columns): new list for each column: if column is in list: do nothing if plain column: add it to the list if grouped column: call mapGroup(column) moveColumns(list): for index=0 to list size: move column from list(index) to index mapGroup(column): find group for column add group column to list (eg "{address}") for each column in group: if plain column: add it to the list if grouped column: recursive call to mapGroup(column)
PanelViewActions
lets the different views share the same toolbar.TreeDisplayModeActions
lets the tree views share actions to switch display modes.NestedTableUtil
is used for the nested table headers, both in the nested view and in the details tree.DataSetStatusBarSynchronizer
is used to synchronize the statusbar across the viewers, primarily to keep track of Max Rows.Test/Debug
There are quite a few test classes that read JSON files to verify structure and behavior. Many classes also implement a dump()
method that can
be called using debug log or on failure in the test classes. These methods typically render the models, grids and headers in a readable format.
TreeTableModel
) and
the features of the list-based DataSet
. One effect of this is AbstractAdaptableTreeTableModel.AbstractTreeTableListRow.getValueAt(row, col)
,
where we have to override the superclass method to return the proper value.
DefaultAdaptableTreeTableGridActionHandler
and PanelViewActions
implement the common logic for the toolbarsDataSetStatusBarSynchronizer
connect the status bars and keeps them in sync.NestedColumnManager.getClosestSingularParentInfo()
scans the tree upwards to find the closest element that is not iterable and
can be used as the root.user
object would be named {user}
, just as the existing property:{"user": {"first name": "Carl","last name": "Hamilton"}, "{user}": "carl.hamilton"}