The JSON/BSON Viewer/Editor

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):

UML overview

Entry Points

Models

Views

Listeners

Since the views and panels are implemented as independent dockable frames, the views don't contain the panels they use to show details. The panels are connected when initialized; these are a few interesting places:

Nested View

This is probably the view that need the most explanation. It is essentially a regular dataset grid using JIDE's NestedTableHeader and TableColumnGroup classes to render columns in nested groups that can be expanded or collapsed.

Expand/Collapse Table Column 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,

Iterable Values

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: [...]).

Identifiers

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

Column Order

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    street
This 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/street
This 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 |

Assemble Column Groups

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 TableColumnGroups).

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)

Utils

In addition to the core classes, there are a few helpers and utils to let the views share some common features without enforcing a common superclass:

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.

Quirks