Patrice Sechere...'s profilePatrice SecheressePhotosBlogLists Tools Help

Patrice Secheresse

Patrice Secheresse - IT Experience
March 11

Drag and Drop - Transferable

Here I will go into the Transferable implementation to be able to transfer a DefaultMutableTreeNode and its content, including the children.

Custom DataFlavor

A Transferable bundle the data to transfer and can restitute it depending on the requested DataFlavor.

The Dnd support includes few basic classes to transfer a string, a file list or an image. If you want to transfer your own object type, there are some MIME types you can use to create your own DataFlavor with the corresponding DataFlavor constructor.

  • javaJVMLocalObjectMimeType: to transfer any object within the same JVM
  • javaRemoteObjectMimeType:to pass a live link to a Remote object (RMI)
  • javaSerializedObjectMimeType: A MIME Content-Type of application/x-java-serialized-object represents a graph of Java object(s) that have been made persistent.

Transferable implementation

The implementation including a string flavor is:

   1: package dnd;
   2:  
   3: import java.awt.datatransfer.DataFlavor;
   4: import java.awt.datatransfer.Transferable;
   5: import java.awt.datatransfer.UnsupportedFlavorException;
   6: import java.io.IOException;
   7: import javax.swing.tree.DefaultMutableTreeNode;
   8:  
   9: /**
  10:  * A Transferable Mutable Tree node.
  11:  * <p>This Transferable bundle a DefaultMutableTreeNode to use into a JTree Drag and Drop
  12:  * operation.</p>
  13:  * 
  14:  */
  15: public class TransferableNode implements Transferable
  16: {
  17:  
  18:     public final static DataFlavor FLAVOR =
  19:             new DataFlavor(DefaultMutableTreeNode.class, "Tree node");
  20:  
  21:     private static final DataFlavor flavors[] =
  22:     {
  23:         FLAVOR,
  24:         DataFlavor.stringFlavor
  25:     };
  26:  
  27:     private final DefaultMutableTreeNode treeNode;
  28:  
  29:     public TransferableNode(DefaultMutableTreeNode treeNode)
  30:     {
  31:         this.treeNode = treeNode;
  32:     }
  33:  
  34:     public DataFlavor[] getTransferDataFlavors()
  35:     {
  36:         return flavors;
  37:     }
  38:  
  39:     public boolean isDataFlavorSupported(DataFlavor flavor)
  40:     {
  41:         return flavor.equals(FLAVOR) || flavor.equals(DataFlavor.stringFlavor);
  42:     }
  43:  
  44:     public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
  45:     {
  46:         if (flavor.equals(FLAVOR))
  47:         {
  48:             return treeNode;
  49:         } else if (flavor.equals(DataFlavor.stringFlavor))
  50:         {
  51:             return treeNode.getUserObject().toString();
  52:         } else
  53:         {
  54:             throw new UnsupportedFlavorException(flavor);
  55:         }
  56:     }
  57: }
The line 18 declares a new DataFlavor FLAVOR for the TransferableNode class. It allows to transfer a serialized DefaultMutableTreeNode with the MIME type "application/x-java-serialized-object". Warning: the user object inside the node must be serializable. Otherwise, you can use create the flavor as a javaJVMLocalObjectMimeType with:

public static DataFlavor FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType +
    "; class=dnd.TransferableNode", "Transferable Node");

In this case, the DnD will be effective only inside the same JVM.

The rest of the code implements the Transferable interface.

The creator line 21 takes a node and encapsulate it into the instance.

The line 34 implements the Transferable method getTransferDataFlavors that returns the flavors that our class can transfer. For example, I added the stringFlavor and will return a string value of the node's user object when a DnD accepts only the text value. The list must start by the more complete representation.

The line 39 is a shortcut to check that a requested data flavor is available. An generic implementation would look up into the array.

Finally, starting line 44, the method returns the node value. To support the text, we have to check the flavor and return the toString value of the user object.

The TransferHandler

To close this example, we need a TransferHandler that use can use the TransferableNode. The following code can transfer our DefaultMutableTreeNode from any JTree, even in a different JVM that has this TreeNodeTransferHandler attached.

Note that the method getSourceActions returns only MOVE so the copy is not allowed and the method exportDone is here to clean up after the transfer.

package dnd;

import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;

/**
* TransferHandler to reorganize the nodes inside the tree
* @author patricesecheresse
*/
public class TreeNodeTransferHandler extends TransferHandler
{

    @Override
    protected Transferable createTransferable(JComponent c)
    {
        JTree tree = (JTree) c;
        TreePath[] paths = tree.getSelectionPaths();
        if (paths != null && paths.length > 0)
        {
            DefaultMutableTreeNode node =
                    (DefaultMutableTreeNode) paths[0].getLastPathComponent();
            return new TransferableNode(node);
        }
        return null;
    }

    @Override
    public int getSourceActions(JComponent c)
    {
        return MOVE;
    }

    @Override
    public boolean canImport(TransferHandler.TransferSupport support)
    {
        if (!support.isDataFlavorSupported(TransferableNode.FLAVOR) ||
                !support.isDrop())
        {
            return false;
        }

        JTree.DropLocation dropLocation =
                (JTree.DropLocation) support.getDropLocation();
        if (dropLocation.getPath() == null)
        {
            return false;
        }

        return dropLocation.getPath() != null;
    }

    @Override
    public boolean importData(TransferHandler.TransferSupport support)
    {
        if (!canImport(support))
        {
            return false;
        }

        JTree tree = (JTree) support.getComponent();
        DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
        JTree.DropLocation dropLocation =
                (JTree.DropLocation) support.getDropLocation();
        TreePath path = dropLocation.getPath();
        Transferable transferable = support.getTransferable();
        DefaultMutableTreeNode transferData;
        try
        {
            transferData = (DefaultMutableTreeNode) transferable.getTransferData(TransferableNode.FLAVOR);
        } catch (IOException e)
        {
            return false;
        } catch (UnsupportedFlavorException e)
        {
            return false;
        }

        int childIndex = dropLocation.getChildIndex();
        if (childIndex == -1)
        {
            childIndex = model.getChildCount(path.getLastPathComponent());
        }

        DefaultMutableTreeNode newNode =
                (DefaultMutableTreeNode) transferData;
        DefaultMutableTreeNode parentNode =
                (DefaultMutableTreeNode) path.getLastPathComponent();
        model.insertNodeInto(newNode, parentNode, childIndex);

        TreePath newPath = path.pathByAddingChild(newNode);
        tree.makeVisible(newPath);
        tree.scrollRectToVisible(tree.getPathBounds(newPath));

        return true;
    }

    @Override
    protected void exportDone(JComponent source, Transferable data, int action)
    {
        if (action == MOVE)
        {
            JTree tree = (JTree) source;
            DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
            DefaultMutableTreeNode transferData;
            try
            {
                transferData = (DefaultMutableTreeNode) data.getTransferData(TransferableNode.FLAVOR);
                model.removeNodeFromParent(transferData);
            } catch (IOException e)
            {
                return;
            } catch (UnsupportedFlavorException e)
            {
                return;
            }
        }

    }
}

Conclusion

Despite being a the simple concept, the drag and drop, especially with rich components like trees,  can be challenging when implemented in real world applications. Even with the Java 6 improvements, the complexity of the underlying plumbing requires a lot of knowledge.

March 09

Drag and Drop - copy/move to the same JTree

Back again to the TransferHandler discovery, one method that we can override is getSourceActions(JComponent c). This method is described with this JavaDoc comment:

Returns the type of transfer actions supported by the source; any bitwise-OR combination of COPY, MOVE and LINK. Some models are not mutable, so a transfer operation of MOVE should not be advertised in that case. Returning NONE disables transfers from the component.

Parameters: c - the component holding the data to be transferred; provided to enable sharing of TransferHandlers

Returns: COPY if the transfer property can be found, otherwise returns NONE

The default implementation of the getSourceActions checks by introspection if the TransferHandler can access a property containing the transferable data. If yes, it returns COPY otherwise it returns NONE to disallow the drop from the drag source.

Now, I need to create a component based on aJTree where I can move the nodes to reorganize the hierarchy. For example, imagine a leaf that you can select and move to the root. It is easy to implements the TransferHandler to manage to drag and drop from others components, including other JTree. Unfortunately, the only problem is when you want to move from the same JTree, it does not work. When the source is attached to your own TransferHandler, you need to advert the actions that you support for the drag.

So, an easy fix to overcome the problem is to override the getSourceAction by returning MOVE. This means you allow the source node to be deleted from its origin and added to the drop destination. If you want to allow a node to be cloned and attached to another part of the tree, you can return COPY.

For example, to allow the source to be copied or moved:

@Override
public int getSourceActions(JComponent c) {

    return COPY_OR_MOVE;
}

The values set in this method will be used by the export with the support.getSourceDropActions().

Drag and Drop with Java 6 - the basics

Swing Drag and Drop

The Drag and Drop aka Dnd has been improved since Java 5 and Java 6. It is old story since the Shannon Hickey blog but as I need to play with it for my current project, it is time to investigate the matter.

Today, it is quite simple to enable the Dnd for basic text component, simply use the setDragEnabled(true) and it's done. You can forget all about Java 1.2 DragGestureRecognizer and all the old stuff associated that you can still find in many old books and tutorials.

However, when it comes to more complex components like JList, JTable and JTree, some extra work is needed and if you dig into the latest Java tutorial, you will find that the subject cannot be covered in 2 minutes.

To start, I will create the simplest code that allows me to drag a text from one tree to another.

The core: the TransferHandler class

The drag exports the data from the source and the drop imports the data into the target through the TransferHandler class. This class contains all the code to do the most of the job and should be extended to set up your own Dnd.

First, create your class that extends TransferHandler and attach an instance to your component, for example, to transfer from a tree to another tree:

MyTransferHandler transferHandler = new MyTransferHandler();

jTree1.setDragEnabled(true); //allow the select node(s) to be dragged

jTree2.setTransferHandler(transferHandler); //add the Dnd mechanism

The export

This part is all about the drag source. The TransferHandler instance has to get your data from your source component. You must bundle it into a class that implements Transferable. This interface is designed to return the data in different formats. A data format to transfer is called a DataFlavor and, for example, the DataFlavor.stringFlavor is provided to bundle text. Of course, you can create your own DataFlavor that carry more complex data structures. Thus, your Transferable implementation can have different data structures to be able to provide the required format for different targets. For now, like most of the examples you will find, we will allow only to transfer plain text by using the existing transferable StringSelection class to carry a stringFlavor.

So, the method to override for our export implementation in the transfer handler is:@Override
public Transferable createTransferable(final JComponent c)
{
    return new StringSelection(c.toString());
}

This creates a Transferable that encapsulates a string version of the drag source component, as defined be the JavaDoc : "Creates a Transferable to use as the source for a data transfer. Returns the representation of the data to be transferred".

The import

This part is all about the drop component. The TransferHandler instance  must know if the source can be imported into the drop component. To do so, we have to override the canImport method "with a return value of true indicating that the transfer represented by the given TransferSupport (which contains all of the details of the transfer) is acceptable". For now, we accept only the string data so the code is:@Override
public boolean canImport(TransferSupport support)
{
    // Check for String flavor
    if (!support.isDataFlavorSupported(stringFlavor))
    {
        return false;
    }
    return true;
}

The TransferSupport is a key class that provides all you need to drop the data. Here, we simply ask if the stringFlavor is accepted.

The last bit of code takes the data and insert it into the tree. To implement it, we override the method importData(TransferSupport support). This method causes a transfer to occur from a drag and drop operation. The Transferable to be imported and the component to transfer to are contained within the TransferSupport.

First, we obtain the data from the support:Transferable t = support.getTransferable();
String data = (String) t.getTransferData(stringFlavor);

Second, we need to know were the drop occurs. A special class exists to help us: JTree.DropLocation. Its getPath() and getChildIndex() methods returns were we should insert the new element. The rest of the code is the usual code for inserting a node:

@Override
public boolean importData(TransferSupport support)
{
    if (!canImport(support))
    {
        return false;
    }

    try
    {
        // Get the tree and model
        JTree targetTree = (JTree) support.getComponent();
        DefaultTreeModel model = ((DefaultTreeModel) targetTree.getModel());
        // Fetch the Transferable and its data
        Transferable t = support.getTransferable();
        String data = (String) t.getTransferData(stringFlavor);
        // Get the node destination
        JTree.DropLocation dropLocation =
                (JTree.DropLocation) support.getDropLocation();

        TreePath path = dropLocation.getPath();
        if (path == null)
        {
            path = new TreePath(model.getRoot());
        }
        int childIndex = dropLocation.getChildIndex();
        if (childIndex == -1)
        {
            childIndex = model.getChildCount(path.getLastPathComponent());
        }

        // create the new node and insert it
        DefaultMutableTreeNode newNode =
                new DefaultMutableTreeNode(data);
        DefaultMutableTreeNode parentNode =
                (DefaultMutableTreeNode) path.getLastPathComponent();
        model.insertNodeInto(newNode, parentNode, childIndex);
        return true;
    } catch (UnsupportedFlavorException ex)
    {
        Logger.getLogger(MyTransferHandler.class.getName()).log(Level.SEVERE, "Not supported.", ex);
    } catch (IOException ex)
    {
        Logger.getLogger(MyTransferHandler.class.getName()).log(Level.SEVERE, "IO exception", ex);
    }
    return false;
}

Conclusion

Despite covering the minimum, this little article takes certainly more than 2 minutes to read and the code is too lengthy to be published here. This shows that the topic is not so simple and more reading is required. While I was finishing this post, I found that a similar article exists in the Java Tech Tips that could have saved me to do all the research and this post.

October 23

Improving the NetBeans unit test support

My current project includes over 1000 unit tests and hundreds of GUI tests. To run the full test suites, it takes more than 25 minutes. NetBeans shows some weaknesses in that area:

  • unable to see the result of each test as soon as the test is executed so you can interrupt as soon as you see a critical error
  • unable to keep the history of previous runs
  • unable to run only the failing tests from a previous test suite from the history
  • unable to run only one test method in a test case from the history

Thankfully, the NetBeans team has accepted to improve the support in the next version 7.

If you found that this list is incomplete or you have a great idea, you can add a comment to this issue.

August 25

BeansBinding: validation error

To continue the previous blog, I will add some explanations about the validator.

For example, I want to validate the age to ensure that it is between 0 and 150. All you need to do is create a class that extends Validator and implements the method validate().

Here the class AgeValidator:

   1: public class AgeValidator extends Validator<Integer> {
   2:  
   3:     @Override
   4:     public Result validate(Integer value) {
   5:         if (value < 0 || value > 150) {
   6:             return new Result(1, "The age must be between 0 and 150. The value " + value + " is incorrect.");
   7:         }
   8:         return null;
   9:     }
  10: }

The method returns a null value if the value is valid.

If an error occurs, you create a result object with a code and/or a description. The code can be useful when you internationalise the application. It could be any object or null.

To add the validator, you just have to set it in the binding: binding.setValidator(new AgeValidator());

So, if you add these lines at the end of our previous run method:

text.setText("151"); //incorrect value, print failure
text.setText("-1"); //incorrect value, print failure

The result will be:

Failure VALIDATION_FAILED: org.jdesktop.beansbinding.Validator$Result [errorCode=1, description=The age must be between 0 and 150. The value 151 is incorrect.]
Failure VALIDATION_FAILED: org.jdesktop.beansbinding.Validator$Result [errorCode=1, description=The age must be between 0 and 150. The value -1 is incorrect.]

You can notice that the type of error is now VALIDATION_FAILED instead of CONVERSION_FAILED.

The validation occurs after the conversion. The idea is to work with the source object type to keep the validation independent of the UI. However, this lead to 2 different systems:

  • conversion error, based on a RuntimeException to indicate a problem with the conversion
  • validation error, based on Result object to indicate an incorrect state of the resulting object after conversion

You have to check the failure and display the error depending of the type. Using an unified way to send a failure would have been simpler.

The full code of the main class is:

   1: public class Main {
   2:  
   3:     Person person;
   4:     Text text;
   5:  
   6:     public Main() {
   7:         person = new Person("John", 21);
   8:         text = new Text();
   9:     }
  10:  
  11:     /**
  12:      * @param args the command line arguments
  13:      */
  14:     public static void main(String[] args) {
  15:         Main test = new Main();
  16:         test.run();
  17:  
  18:     }
  19:  
  20:     private void run() {
  21:         Property ageProperty = BeanProperty.create("age");
  22:         Property textProperty = BeanProperty.create("text");
  23:         // bind the persone age to a text component
  24:         Binding binding = Bindings.createAutoBinding(UpdateStrategy.READ_WRITE,
  25:                 person, ageProperty, text, textProperty);
  26:         binding.setValidator(new AgeValidator());
  27:         binding.addBindingListener(new AbstractBindingListener() {
  28:  
  29:             @Override
  30:             public void syncFailed(Binding binding, SyncFailure failure) {
  31:                 System.err.println("Failure " + failure);
  32:             }
  33:         });
  34:         binding.bind();
  35:  
  36:         text.setText("22"); // print value
  37:         text.setText("22 and half"); // incorrect value, print failure
  38:         text.setText("151"); //incorrect value, print failure
  39:         text.setText("-1"); //incorrect value, print failure
  40:     }
 
by 
by 

Feed

The owner hasn't specified a feed for this module yet.

Patrice Secheresse

Occupation
Location
Interests
Experienced Project Manager and Analyst Programmer.
Management of successful projects in commercial areas.