JDK-6392086 : Copy to clipboard with html mime type fails with input stream
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.awt
  • Affected Version: 6
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2006-02-28
  • Updated: 2011-03-07
  • Resolved: 2011-03-07
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7
7 b07Fixed
Description
FULL PRODUCT VERSION :
java version "1.6.0-beta2"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.6.0-beta2-b72)
Java HotSpot(TM) Client VM (build 1.6.0-beta2-b72, mixed mode, sharing)

also

java version "1.5.0_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05)
Java HotSpot(TM) Client VM (build 1.5.0_06-b05, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
Probably all other windows versions, too.

A DESCRIPTION OF THE PROBLEM :
When using an input stream (the default class) as transfer data copying data of "text/html" mime types is not possible at all.

Java erroneously duplicates text in the native clipboard format:

Version:0.9
StartHTML:-1
EndHTML:-1
StartFragment:0000000111
EndFragment:0000000332
<!--StartFragment-->
Version:0.9
StartHTML:-1
EndHTML:-1
StartFragment:0000000111
EndFragment:0000000201
<!--StartFragment-->
The quick <font color='#78650d'>brown</font> <b>mouse</b> jumped over the lazy <b>cat</b>.<!--EndFragment-->
<!--EndFragment-->

This issue is described in more detail on
http://www.peterbuettner.de/develop/javasnippets/clipHtml/index.html

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the code below to fill the clipboard.
Try to paste in OpenOffice 2.0 Writer. Notice the error message "Requested clipboard format is not available".
Try to paste in MS Word 2003. The following text is inserted:
"Version:0.9 StartHTML:-1 EndHTML:-1 StartFragment:0000000111 EndFragment:0000000201 The quick brown mouse jumped over the lazy cat."

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
In both OpenOffice Writer and MS Word (and any other word processor capable of dealing with html type) the formatted text should be inserted.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;

public class HtmlStreamCopy {
    public static void main(String[] args) throws ClassNotFoundException {
        String htmlText = "The quick <font color='#78650d'>brown</font> <b>mouse</b> jumped over the lazy <b>cat</b>.";
        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        Transferable htmlTransferable = new HtmlStreamCopy.HtmlInputStreamTransferable(htmlText);
        clipboard.setContents(htmlTransferable, null);
    }

    private static class HtmlInputStreamTransferable implements Transferable {
        private final DataFlavor _htmlDataFlavor;
        private final String _htmlText;

        public HtmlInputStreamTransferable(String htmlText) throws ClassNotFoundException {
            _htmlText = htmlText;
            _htmlDataFlavor = new DataFlavor("text/html");
        }

        public DataFlavor[] getTransferDataFlavors() {
            return new DataFlavor[]{_htmlDataFlavor};
        }

        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return "text/html".equals(flavor.getMimeType());

        }

        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
        {
            InputStream stringStream = new ByteArrayInputStream(_htmlText.getBytes("utf-8"));
            return stringStream;
        }
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Use a string instead of an input stream as transfer object.

Comments
SUGGESTED FIX The approved webrev revision was copied to NFS: /net/jano.sfbay/export/disk29/jcg/1.7.0-dolphin/awt/6392086 URL: http://javaweb.sfbay/jcg/1.7.0-dolphin/awt/6392086/
16-01-2007

EVALUATION The recursive action looks like that: WDataTransfer::translateTransferable(...) { +HTML header for CF_HTML native format } --(super with HTMLFlavor)-->DataTransfer::translateTransferable() --(if flavor.isRepresentationClassInputStream() call translateTransferable again with plainTextFlavor)-->WDataTransfer::translateTransferable(...){ +second HTML header for CF_HTML native format - that is a problem!} --(super with plainTextFlavor)-->DataTransfer::translateTransferable() Using of recursion has no reason at that place because it needs just for particular InputStream to byte[] conversion. Now it is realized as separate translateTransferableString function. The same problem was in the translateBytesOrStream function that is using at clipboard data extraction. It does not produce an error yet, but gets lack in performance and creates a big risk for any platform-dependent modification. HTML format is the only format with custom platform action at translateTransferable stage: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException { byte[] bytes = super.translateTransferable(contents, flavor, format); if (format == CF_HTML) { bytes = HTMLSupport.convertToHTMLFormat(bytes); } return bytes; } The clipboard data exchange in JAVA is different for in-process and out-process communications. We most of clipboard support subroutines executing entirely just in case of out-process interaction. That is why HTMLTransferTest.java need to run child process. System.err was chosen by historical reason: ImageTransferTest is a direct child of ImageTransferTest. DnDHTMLToOutlookTest is valid for X too. Word and Outlook mentioned as optional program. Any HTML document editor are acceptable at any platform. But MS Outlook and Word for Window platform would provide more extended coverage for test in future, when HTML clipboard format ver 1.0 will accepted for review. The tests were designed with aim to cover backward compatibility and compatibility with native applications while changing HTML clipboard format.
25-12-2006

SUGGESTED FIX There is "fast and dirty" fix for Java Service pack, but mail stream solution have to be more accurate. *** %root%/src/windows/classes/sun/awt/windows/WDataTransferer.java *** 158,172 **** public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException { ! byte[] bytes = super.translateTransferable(contents, flavor, format); if (format == CF_HTML) { ! bytes = HTMLSupport.convertToHTMLFormat(bytes); ! } return bytes; } protected Object translateBytesOrStream(InputStream str, byte[] bytes, --- 158,175 ---- public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException { ! byte[] bytes = null; if (format == CF_HTML) { ! bytes = HTMLSupport.convertToHTMLFormat( ! super.translateTransferable(contents, flavor, CF_TEXT) ! ); ! } else ! bytes = super.translateTransferable(contents, flavor, format); return bytes; } protected Object translateBytesOrStream(InputStream str, byte[] bytes,
21-12-2006

EVALUATION The main problem is in recursion call of public virtual function public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException in src\share\classes\sun\awt\datatransfer\DataTransferer.java that is reimplemeted in src\windows\classes\sun\awt\windows\WDataTransferer.java That leads to doubled HTML format-head concatination with clipboard data stream. This practice does not correspond to good software design and have to be rewritten. But "two-bytes fix" is availuable for service packs (look to the "Suggested fix"). Correct fix includes translateBytesOrStream and translateTransferable functions rewriting to avoid the recursion. A number of tests have to be implemented to test backward compartibility as well as interaction with native applications in clipboard and DnD operations. HTML text transport have to be upgraded, but this is a subject for another task.
21-12-2006

EVALUATION the problem are in wrong ���virtual��� recursion. correction in WDataTransfer.java provides dirty solution for the problem -------------------------------------------- public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException { if (format == CF_HTML) { return HTMLSupport.convertToHTMLFormat( super.translateTransferable(contents, flavor, CF_TEXT) ); } return super.translateTransferable(contents, flavor, format); } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To extend HTML clipboard format compatibility I would like to implement following fix in WDataTransfer.java (additional changes in HTMLDecodingInputStream class are required!): -------------------------------------------- final class HTMLSupport { public static final String ENCODING = "UTF-8"; public static final String VERSION = "Version:"; public static final String START_HTML = "StartHTML:"; public static final String END_HTML = "EndHTML:"; public static final String START_FRAGMENT = "StartFragment:"; public static final String END_FRAGMENT = "EndFragment:"; public static final String HTML_PREFIX = "<HTML><BODY>"; public static final String HTML_SUFFIX = "</BODY></HTML>"; public static final String START_FRAGMENT_CMT = "<!--StartFragment-->"; public static final String END_FRAGMENT_CMT = "<!--EndFragment-->"; public static final String SOURCE_URL = "SourceURL:"; public static final String DEF_SOURCE_URL = "about:blank"; public static final String EOLN = "\r\n"; private static final String VERSION_NUM = "1.0"; private static final int PADDED_WIDTH = 10; private static String toPaddedString(int n, int width) { String string = "" + n; int len = string.length(); if (n >= 0 && len < width) { char[] array = new char[width - len]; Arrays.fill(array, '0'); StringBuffer buffer = new StringBuffer(); buffer.append(array); buffer.append(string); string = buffer.toString(); } return string; } public static byte[] convertToHTMLFormat(byte[] bytes) { // Calculate section offsets String stBaseUrl = DEF_SOURCE_URL; int nStartHTML = VERSION.length() + VERSION_NUM.length() + EOLN.length() + START_HTML.length() + PADDED_WIDTH + EOLN.length() + END_HTML.length() + PADDED_WIDTH + EOLN.length() + START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() + END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() + SOURCE_URL.length() + stBaseUrl.length() + EOLN.length() ; int nStartFragment = nStartHTML + HTML_PREFIX.length(); int nEndFragment = nStartFragment + START_FRAGMENT_CMT.length() + bytes.length - 1 + END_FRAGMENT_CMT.length() ; int nEndHTML = nEndFragment + HTML_SUFFIX.length(); StringBuffer header = new StringBuffer( nStartFragment + START_FRAGMENT_CMT.length() ); //header header.append(VERSION); header.append(VERSION_NUM); header.append(EOLN); header.append(START_HTML); header.append(toPaddedString(nStartHTML, PADDED_WIDTH)); header.append(EOLN); header.append(END_HTML); header.append(toPaddedString(nEndHTML, PADDED_WIDTH)); header.append(EOLN); header.append(START_FRAGMENT); header.append(toPaddedString(nStartFragment, PADDED_WIDTH)); header.append(EOLN); header.append(END_FRAGMENT); header.append(toPaddedString(nEndFragment, PADDED_WIDTH)); header.append(EOLN); header.append(SOURCE_URL); header.append(stBaseUrl); header.append(EOLN); //HTML header.append(HTML_PREFIX); header.append(START_FRAGMENT_CMT); byte[] headerBytes = null, trailerBytes = null; try { headerBytes = new String(header).getBytes(ENCODING); trailerBytes = (END_FRAGMENT_CMT + HTML_SUFFIX).getBytes(ENCODING); } catch (UnsupportedEncodingException cannotHappen) { } byte[] retval = new byte[headerBytes.length + bytes.length + trailerBytes.length]; System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length); System.arraycopy(bytes, 0, retval, headerBytes.length, bytes.length - 1); System.arraycopy(trailerBytes, 0, retval, headerBytes.length + bytes.length - 1, trailerBytes.length); retval[retval.length-1] = 0; return retval; } } ..... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
02-10-2006