Making a JEditorPane with html put correctly formatted text in clipboard

After getting no answers, I rolled up my sleeves and did a lot of research and learning. The solution is to make a custom TransferHandler for the component, and massage the HTML text manually. It wasn't easy to work all this out, which could account for the zero answers I got.

Here's a working solution:

import javax.swing.*;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;

public class ScratchSpace {

    public static void main(String[] args) {
        final JFrame frame = new JFrame();
        final JEditorPane pane = new JEditorPane("text/html", "<html><font color=red>Hello</font><br>\u2663<br>World");
        pane.setTransferHandler(new MyTransferHandler());
        frame.getContentPane().add(pane);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

}

class MyTransferHandler extends TransferHandler {

    protected Transferable createTransferable(JComponent c) {
        final JEditorPane pane = (JEditorPane) c;
        final String htmlText = pane.getText();
        final String plainText = extractText(new StringReader(htmlText));
        return new MyTransferable(plainText, htmlText);
    }

    public String extractText(Reader reader) {
        final ArrayList<String> list = new ArrayList<String>();

        HTMLEditorKit.ParserCallback parserCallback = new HTMLEditorKit.ParserCallback() {
            public void handleText(final char[] data, final int pos) {
                list.add(new String(data));
            }

            public void handleStartTag(HTML.Tag tag, MutableAttributeSet attribute, int pos) {
            }

            public void handleEndTag(HTML.Tag t, final int pos) {
            }

            public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, final int pos) {
                if (t.equals(HTML.Tag.BR)) {
                    list.add("\n");
                }
            }

            public void handleComment(final char[] data, final int pos) {
            }

            public void handleError(final String errMsg, final int pos) {
            }
        };
        try {
            new ParserDelegator().parse(reader, parserCallback, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String result = "";
        for (String s : list) {
            result += s;
        }
        return result;
    }


    @Override
    public void exportToClipboard(JComponent comp, Clipboard clip, int action) throws IllegalStateException {
        if (action == COPY) {
            clip.setContents(this.createTransferable(comp), null);
        }
    }

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

}

class MyTransferable implements Transferable {

    private static final DataFlavor[] supportedFlavors;

    static {
        try {
            supportedFlavors = new DataFlavor[]{
                    new DataFlavor("text/html;class=java.lang.String"),
                    new DataFlavor("text/plain;class=java.lang.String")
            };
        } catch (ClassNotFoundException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final String plainData;
    private final String htmlData;

    public MyTransferable(String plainData, String htmlData) {
        this.plainData = plainData;
        this.htmlData = htmlData;
    }

    public DataFlavor[] getTransferDataFlavors() {
        return supportedFlavors;
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
        for (DataFlavor supportedFlavor : supportedFlavors) {
            if (supportedFlavor == flavor) {
                return true;
            }
        }
        return false;
    }

    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
        if (flavor.equals(supportedFlavors[0])) {
            return htmlData;
        }
        if (flavor.equals(supportedFlavors[1])) {
            return plainData;
        }
        throw new UnsupportedFlavorException(flavor);
    }
}

Thank you for your code post! I am working on getting an app up and running under JNLP that allows the user to create MLA citations and then copy/paste them into a word processor. So the formatting needs to be preserved.

See http://proctinator.com/citation/

There is an easier way, but I think I will need the sort of approach you demonstrate above to get my application working with jnlp.

The code below works find for a JEditorPane running in an unrestricted environment. But copy/paste is not directly available when you app is in a sandbox (such as would be the case for an applet or JNLP file that did not request full permissions.)

JEditorPane citEditorPane;
//user fills pane with MLA citations.
citEditorPane.selectAll();
citEditorPane.copy();
citEditorPane.select(0, 0);

Note: this is not an answer to the question, just a comment with code to the answer by @Thorn, related to security restrictions

In webstartables with default permissions (that is, none ;-) you can ask the SecurityManager at runtime for a ClipboardService: it will popup a dialog asking the user once to allow (or disallow) the copy. With that, you can replace the default copy action in the textComponent. In SwingX demo we support pasting the code from the source area by:

/**
 * Replaces the editor's default copy action in security restricted
 * environments with one messaging the ClipboardService. Does nothing 
 * if not restricted.
 * 
 * @param editor the editor to replace 
 */
public static void replaceCopyAction(final JEditorPane editor) {
    if (!isRestricted()) return;
    Action safeCopy = new AbstractAction() {

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                ClipboardService cs = (ClipboardService)ServiceManager.lookup
                    ("javax.jnlp.ClipboardService");
                StringSelection transferable = new StringSelection(editor.getSelectedText());
                cs.setContents(transferable);
            } catch (Exception e1) {
                // do nothing
            }
        }
    };
    editor.getActionMap().put(DefaultEditorKit.copyAction, safeCopy);
}

private static boolean isRestricted() {
    SecurityManager manager = System.getSecurityManager();
    if (manager == null) return false;
    try {
        manager.checkSystemClipboardAccess();
        return false;
    } catch (SecurityException e) {
        // nothing to do - not allowed to access
    }
    return true;
}