JavaFX TreeView Drag & Drop

JavaFX’s TreeView is a powerful component, but the code required to implement some of the finer details is not necessarily obvious.

drag-dropThe ability to rearrange tree nodes via drag and drop is a feature that users typically expect in a tree component.  A drag image and a drop location hint should also be employed to enhance usability.  In this post, we’ll explore an example that handles all of these things.

Note to Swing Developers

TreeView is fundamentally different from Swing’s JTree.   While JTree’s cell renderer uses a single component to “rubber stamp” each cell, TreeView’s cells are actual components.  TreeView creates enough cells to satisfy the needs of viewport, and these cells scan be reused as the user scrolls and interacts with the tree.  This approach allows custom cells to be interactive; for example, a cell may contain a clickable button or other component.  Facilitating this type of interaction with JTree required some hackery since the cell was only a “picture” of the actual component.

Creating a TreeView

Creating a TreeView is straightforward.  For the sake of this example, I’ve simply hard coded a few nodes.

TreeItem rootItem = new TreeItem(new TaskNode("Tasks"));
rootItem.setExpanded(true);

ObservableList children = rootItem.getChildren();
children.add(new TreeItem(new TaskNode("do laundry")));
children.add(new TreeItem(new TaskNode("get groceries")));
children.add(new TreeItem(new TaskNode("drink beer")));
children.add(new TreeItem(new TaskNode("defrag hard drive")));
children.add(new TreeItem(new TaskNode("walk dog")));
children.add(new TreeItem(new TaskNode("buy beer")));

TreeView tree = new TreeView(rootItem);
tree.setCellFactory(new TaskCellFactory());

Creating Cells

The cell factory is more interesting. With JTree, drag and drop was registered at the tree level.  With TreeView, the individual cells participate directly.  Drag event handlers must be set for each cell that is created:

cell.setOnDragDetected((MouseEvent event) -> dragDetected(event, cell, treeView));
cell.setOnDragOver((DragEvent event) -> dragOver(event, cell, treeView));
cell.setOnDragDropped((DragEvent event) -> drop(event, cell, treeView));
cell.setOnDragDone((DragEvent event) -> clearDropLocation());

Drag Detected

Inside dragDetected(), we must decide whether a node is actually draggable. If it is, the underlying value is added to the clipboard content.

private void dragDetected(MouseEvent event, TreeCell treeCell, TreeView treeView) {
    draggedItem = treeCell.getTreeItem();

    // root can't be dragged
    if (draggedItem.getParent() == null) return;
    Dragboard db = treeCell.startDragAndDrop(TransferMode.MOVE);

    ClipboardContent content = new ClipboardContent();
    content.put(JAVA_FORMAT, draggedItem.getValue());
    db.setContent(content);
    db.setDragView(treeCell.snapshot(null, null));
    event.consume();
}

Drag Over

Our dragOver() method is triggered when the user is dragging a node over the cell. In this method we must decide whether the node being dragged could be dropped in this location, and if so, set a style on this cell that yields a visual hint as to where the dragged node will be placed if dropped.

private void dragOver(DragEvent event, TreeCell treeCell, TreeView treeView) {
    if (!event.getDragboard().hasContent(JAVA_FORMAT)) return;
    TreeItem thisItem = treeCell.getTreeItem();

    // can't drop on itself
    if (draggedItem == null || thisItem == null || thisItem == draggedItem) return;
    // ignore if this is the root
    if (draggedItem.getParent() == null) {
        clearDropLocation();
        return;
    }

    event.acceptTransferModes(TransferMode.MOVE);
    if (!Objects.equals(dropZone, treeCell)) {
        clearDropLocation();
        this.dropZone = treeCell;
        dropZone.setStyle(DROP_HINT_STYLE);
    }
}

Drag Dropped

If a node is actually dropped, the drop() method handles removing the dropped node from the old location and adding it to the new location.

private void drop(DragEvent event, TreeCell treeCell, TreeView treeView) {
    Dragboard db = event.getDragboard();
    boolean success = false;
    if (!db.hasContent(JAVA_FORMAT)) return;

    TreeItem thisItem = treeCell.getTreeItem();
    TreeItem droppedItemParent = draggedItem.getParent();

    // remove from previous location
    droppedItemParent.getChildren().remove(draggedItem);

    // dropping on parent node makes it the first child
    if (Objects.equals(droppedItemParent, thisItem)) {
        thisItem.getChildren().add(0, draggedItem);
        treeView.getSelectionModel().select(draggedItem);
    }
    else {
        // add to new location
        int indexInParent = thisItem.getParent().getChildren().indexOf(thisItem);
        thisItem.getParent().getChildren().add(indexInParent + 1, draggedItem);
    }
    treeView.getSelectionModel().select(draggedItem);
    event.setDropCompleted(success);
}

Challenges

TreeItem is not serializable, so it cannot be placed on the clipboard when a drag is recognized. Instead, the value object behind the TreeItem is the more likely candidate for the clipboard. This is unfortunate, however, because downstream drag/drop event methods need to know the TreeItem that is being dragged and it would be convenient if it were on the clipboard. We have a couple of choices- store the dragged item in a variable (the approach taken in this example), or search the tree looking for the TreeItem that corresponds to the value object on the clipboard.

Conclusion

Adding D&D-based reordering to a TreeView isn’t difficult once you have the pattern to follow! Find the entire source of this example here.
 

Script Compilation with Nashorn

Many developers know that a new JavaScript engine called Nashorn was introduced in Java 8 as a replacement for the aging Rhino engine.  Recently, I (finally) had the opportunity to make use of the capability.

The project is a custom NiFi processor that utilizes a custom configuration-based data transformation engine.  The configurations make heavy use of JavaScript-based mappings to move and munge fields from a source schema into a target schema.  Our initial testing revealed rather lackluster performance.  JProfiler indicated that the hotspot was the script engine’s eval() method, which really wasn’t that helpful since I already knew that script execution was going to be the long pole in the tent.

It turned out that I had missed an opportunity during the initial implementation.  The Nashorn script engine implements Compilable, a functional interface that allows you to compile your script.

@Test
public void testWithCompilation() throws Exception {
    ScriptEngine engine = mgr.getEngineByName("nashorn");
    CompiledScript compiled = ((Compilable) engine).compile("value = 'junit';");
    for (int i = 0; i < 10000; i++) {
        Bindings bindings = engine.createBindings();
        compiled.eval(bindings);
        Object result = bindings.get("value");
        Assert.assertEquals(result, "junit");
    }
}

@Test
public void testWithoutCompilation() throws Exception {
    for (int i = 0; i < 10000; i++) {
        ScriptEngine engine = mgr.getEngineByName("nashorn");
        engine.eval("value = 'junit';");
        Object result = engine.get("value");
        Assert.assertEquals(result, "junit");
    }
}

junit

As you can see, the difference is substantial across a test of 10,000 invocations.  A batch size of a few million records is pretty ordinary for the system that uses this component, so this represents a huge time savings.

I should also mention that the script engine is thread safe.  For concurrent use, each thread simply needs to obtain a fresh bindings instance from the engine as shown in the code above.

I get the impression that Nashorn may be an underutilized feature in the JDK.  However, script-based extensibility in an application can be quite valuable in certain scenarios.  Nashorn is worth keeping in mind for your future projects.

Lightweight Entity Extractor

Named Entity Recognition (NER) or entity extraction has a wide array of use cases, from processing customer correspondence (help desks, feedback systems, etc.) to data foresnsics.

NER solutions come in all shapes and sizes.   Libraries like GATE and Stanford NLP have been popular options for many years.  Commercial products like NetOwl and Rosette offer enterprise capabilities that can be installed on-premise.  Newcomers such as Amazon Comprehend offer pay-as-you-go cloud-only solutions.

Sometimes a use case calls for extracting everything possible from a document, or the area of concern may be so broad that it isn’t feasible to develop an effective lexicon and set of patterns.  Solutions fit for this problem are typically more complex and involve a lot of behind-the-scenes natural language processing.

In other scenarios, the use case might be more targeted.  For example, perhaps you need to find all occurrences of specific organizations and persons along with any identifable telephone numbers and email addresses.

If you are working with a specific lexicon and set of patterns, some of the larger frameworks or products may introduce an undesirable complexity and/or cost.  The signal to noise ratio may be higher that desired as well.  In these cases, many choose to roll a homegrown solution.  Unfortunately, these solutions are often based exclusively on regex or simple string evaluation and as a result may neither perform well nor yield quality results.

I recently built a lightweight Java library for handling lexicon-based and pattern-based extraction.  It processes a 25K word document with a lexicon consisting of 50K entries in about 130 milliseconds on a mid 2015 MacBook.  Increasing the lexicon to 500K items yields results in around 230 ms.  A sample signature block processed using a targeted lexicon and set of patterns is shown below.

sig-example

Perhaps you’ll find some use for this in your application or data pipeline.  Happy extracting!