1 /*
2  * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.awt.windows;
27 
28 import java.awt.Image;
29 import java.awt.Graphics2D;
30 import java.awt.Transparency;
31 
32 import java.awt.color.ColorSpace;
33 
34 import java.awt.datatransfer.DataFlavor;
35 import java.awt.datatransfer.FlavorTable;
36 import java.awt.datatransfer.Transferable;
37 import java.awt.datatransfer.UnsupportedFlavorException;
38 
39 import java.awt.geom.AffineTransform;
40 
41 import java.awt.image.BufferedImage;
42 import java.awt.image.ColorModel;
43 import java.awt.image.ComponentColorModel;
44 import java.awt.image.DataBuffer;
45 import java.awt.image.DataBufferByte;
46 import java.awt.image.DataBufferInt;
47 import java.awt.image.DirectColorModel;
48 import java.awt.image.ImageObserver;
49 import java.awt.image.Raster;
50 import java.awt.image.WritableRaster;
51 
52 import java.io.BufferedInputStream;
53 import java.io.BufferedReader;
54 import java.io.InputStream;
55 import java.io.InputStreamReader;
56 import java.io.IOException;
57 import java.io.UnsupportedEncodingException;
58 import java.io.File;
59 
60 import java.net.URL;
61 
62 import java.nio.charset.Charset;
63 import java.util.Arrays;
64 import java.util.Collections;
65 import java.util.HashMap;
66 import java.util.Map;
67 import java.util.SortedMap;
68 
69 import sun.awt.Mutex;
70 import sun.awt.datatransfer.DataTransferer;
71 import sun.awt.datatransfer.ToolkitThreadBlockedHandler;
72 
73 import sun.awt.image.ImageRepresentation;
74 import sun.awt.image.ToolkitImage;
75 
76 import java.util.ArrayList;
77 
78 import java.io.ByteArrayOutputStream;
79 
80 /**
81  * Platform-specific support for the data transfer subsystem.
82  *
83  * @author David Mendenhall
84  * @author Danila Sinopalnikov
85  *
86  * @since 1.3.1
87  */
88 final class WDataTransferer extends DataTransferer {
89     private static final String[] predefinedClipboardNames = {
90             "",
91             "TEXT",
92             "BITMAP",
93             "METAFILEPICT",
94             "SYLK",
95             "DIF",
96             "TIFF",
97             "OEM TEXT",
98             "DIB",
99             "PALETTE",
100             "PENDATA",
101             "RIFF",
102             "WAVE",
103             "UNICODE TEXT",
104             "ENHMETAFILE",
105             "HDROP",
106             "LOCALE",
107             "DIBV5"
108     };
109 
110     private static final Map <String, Long> predefinedClipboardNameMap;
111     static {
112         Map <String,Long> tempMap =
113                 new HashMap <> (predefinedClipboardNames.length, 1.0f);
114         for (int i = 1; i < predefinedClipboardNames.length; i++) {
tempMap.put(predefinedClipboardNames[i], Long.valueOf(i))115             tempMap.put(predefinedClipboardNames[i], Long.valueOf(i));
116         }
117         predefinedClipboardNameMap =
118                 Collections.synchronizedMap(tempMap);
119     }
120 
121     /**
122      * from winuser.h
123      */
124     public static final int CF_TEXT = 1;
125     public static final int CF_METAFILEPICT = 3;
126     public static final int CF_DIB = 8;
127     public static final int CF_ENHMETAFILE = 14;
128     public static final int CF_HDROP = 15;
129     public static final int CF_LOCALE = 16;
130 
131     public static final long CF_HTML = registerClipboardFormat("HTML Format");
132     public static final long CFSTR_INETURL = registerClipboardFormat("UniformResourceLocator");
133     public static final long CF_PNG = registerClipboardFormat("PNG");
134     public static final long CF_JFIF = registerClipboardFormat("JFIF");
135 
136     public static final long CF_FILEGROUPDESCRIPTORW = registerClipboardFormat("FileGroupDescriptorW");
137     public static final long CF_FILEGROUPDESCRIPTORA = registerClipboardFormat("FileGroupDescriptor");
138     //CF_FILECONTENTS supported as mandatory associated clipboard
139 
140     private static final Long L_CF_LOCALE =
141             predefinedClipboardNameMap.get(predefinedClipboardNames[CF_LOCALE]);
142 
143     private static final DirectColorModel directColorModel =
144             new DirectColorModel(24,
145                     0x00FF0000,  /* red mask   */
146                     0x0000FF00,  /* green mask */
147                     0x000000FF); /* blue mask  */
148 
149     private static final int[] bandmasks = new int[] {
150             directColorModel.getRedMask(),
151             directColorModel.getGreenMask(),
152             directColorModel.getBlueMask() };
153 
154     /**
155      * Singleton constructor
156      */
WDataTransferer()157     private WDataTransferer() {
158     }
159 
160     private static WDataTransferer transferer;
161 
getInstanceImpl()162     static synchronized WDataTransferer getInstanceImpl() {
163         if (transferer == null) {
164             transferer = new WDataTransferer();
165         }
166         return transferer;
167     }
168 
169     @Override
getFormatsForFlavors( DataFlavor[] flavors, FlavorTable map)170     public SortedMap <Long, DataFlavor> getFormatsForFlavors(
171             DataFlavor[] flavors, FlavorTable map)
172     {
173         SortedMap <Long, DataFlavor> retval =
174                 super.getFormatsForFlavors(flavors, map);
175 
176         // The Win32 native code does not support exporting LOCALE data, nor
177         // should it.
178         retval.remove(L_CF_LOCALE);
179 
180         return retval;
181     }
182 
183     @Override
getDefaultUnicodeEncoding()184     public String getDefaultUnicodeEncoding() {
185         return "utf-16le";
186     }
187 
188     @Override
translateTransferable(Transferable contents, DataFlavor flavor, long format)189     public byte[] translateTransferable(Transferable contents,
190                                         DataFlavor flavor,
191                                         long format) throws IOException
192     {
193         byte[] bytes = null;
194         if (format == CF_HTML) {
195             if (contents.isDataFlavorSupported(DataFlavor.selectionHtmlFlavor)) {
196                 // if a user provides data represented by
197                 // DataFlavor.selectionHtmlFlavor format, we use this
198                 // type to store the data in the native clipboard
199                 bytes = super.translateTransferable(contents,
200                         DataFlavor.selectionHtmlFlavor,
201                         format);
202             } else if (contents.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) {
203                 // if we cannot get data represented by the
204                 // DataFlavor.selectionHtmlFlavor format
205                 // but the DataFlavor.allHtmlFlavor format is avialable
206                 // we belive that the user knows how to represent
207                 // the data and how to mark up selection in a
208                 // system specific manner. Therefor, we use this data
209                 bytes = super.translateTransferable(contents,
210                         DataFlavor.allHtmlFlavor,
211                         format);
212             } else {
213                 // handle other html flavor types, including custom and
214                 // fragment ones
215                 bytes = HTMLCodec.convertToHTMLFormat(super.translateTransferable(contents, flavor, format));
216             }
217         } else {
218             // we handle non-html types basing on  their
219             // flavors
220             bytes = super.translateTransferable(contents, flavor, format);
221         }
222         return bytes;
223     }
224 
225     // The stream is closed as a closable object
226     @Override
translateStream(InputStream str, DataFlavor flavor, long format, Transferable localeTransferable)227     public Object translateStream(InputStream str,
228                                  DataFlavor flavor, long format,
229                                  Transferable localeTransferable)
230         throws IOException
231     {
232         if (format == CF_HTML && flavor.isFlavorTextType()) {
233             str = new HTMLCodec(str,
234                                  EHTMLReadMode.getEHTMLReadMode(flavor));
235 
236         }
237         return super.translateStream(str, flavor, format,
238                                         localeTransferable);
239 
240     }
241 
242     @Override
translateBytes(byte[] bytes, DataFlavor flavor, long format, Transferable localeTransferable)243     public Object translateBytes(byte[] bytes, DataFlavor flavor, long format,
244         Transferable localeTransferable) throws IOException
245     {
246 
247 
248         if (format == CF_FILEGROUPDESCRIPTORA || format == CF_FILEGROUPDESCRIPTORW) {
249             if (bytes == null || !DataFlavor.javaFileListFlavor.equals(flavor)) {
250                 throw new IOException("data translation failed");
251             }
252             String st = new String(bytes, 0, bytes.length, "UTF-16LE");
253             String[] filenames = st.split("\0");
254             if( 0 == filenames.length ){
255                 return null;
256             }
257 
258             // Convert the strings to File objects
259             File[] files = new File[filenames.length];
260             for (int i = 0; i < filenames.length; ++i) {
261                 files[i] = new File(filenames[i]);
262                 //They are temp-files from memory Stream, so they have to be removed on exit
263                 files[i].deleteOnExit();
264             }
265             // Turn the list of Files into a List and return
266             return Arrays.asList(files);
267         }
268 
269         if (format == CFSTR_INETURL &&
270                 URL.class.equals(flavor.getRepresentationClass()))
271         {
272             String charset = Charset.defaultCharset().name();
273             if (localeTransferable != null
274                     && localeTransferable.isDataFlavorSupported(javaTextEncodingFlavor))
275             {
276                 try {
277                     charset = new String((byte[])localeTransferable.
278                         getTransferData(javaTextEncodingFlavor), "UTF-8");
279                 } catch (UnsupportedFlavorException cannotHappen) {
280                 }
281             }
282             return new URL(new String(bytes, charset));
283         }
284 
285         return super.translateBytes(bytes , flavor, format,
286                                         localeTransferable);
287 
288     }
289 
290     @Override
isLocaleDependentTextFormat(long format)291     public boolean isLocaleDependentTextFormat(long format) {
292         return format == CF_TEXT || format == CFSTR_INETURL;
293     }
294 
295     @Override
isFileFormat(long format)296     public boolean isFileFormat(long format) {
297         return format == CF_HDROP || format == CF_FILEGROUPDESCRIPTORA || format == CF_FILEGROUPDESCRIPTORW;
298     }
299 
300     @Override
getFormatForNativeAsLong(String str)301     protected Long getFormatForNativeAsLong(String str) {
302         Long format = predefinedClipboardNameMap.get(str);
303         if (format == null) {
304             format = Long.valueOf(registerClipboardFormat(str));
305         }
306         return format;
307     }
308 
309     @Override
getNativeForFormat(long format)310     protected String getNativeForFormat(long format) {
311         return (format < predefinedClipboardNames.length)
312                 ? predefinedClipboardNames[(int)format]
313                 : getClipboardFormatName(format);
314     }
315 
316     private final ToolkitThreadBlockedHandler handler =
317             new WToolkitThreadBlockedHandler();
318 
319     @Override
getToolkitThreadBlockedHandler()320     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
321         return handler;
322     }
323 
324     /**
325      * Calls the Win32 RegisterClipboardFormat function to register
326      * a non-standard format.
327      */
registerClipboardFormat(String str)328     private static native long registerClipboardFormat(String str);
329 
330     /**
331      * Calls the Win32 GetClipboardFormatName function which is
332      * the reverse operation of RegisterClipboardFormat.
333      */
getClipboardFormatName(long format)334     private static native String getClipboardFormatName(long format);
335 
336     @Override
isImageFormat(long format)337     public boolean isImageFormat(long format) {
338         return format == CF_DIB || format == CF_ENHMETAFILE ||
339                 format == CF_METAFILEPICT || format == CF_PNG ||
340                 format == CF_JFIF;
341     }
342 
343     @Override
imageToPlatformBytes(Image image, long format)344     protected byte[] imageToPlatformBytes(Image image, long format)
345             throws IOException {
346         String mimeType = null;
347         if (format == CF_PNG) {
348             mimeType = "image/png";
349         } else if (format == CF_JFIF) {
350             mimeType = "image/jpeg";
351         }
352         if (mimeType != null) {
353             return imageToStandardBytes(image, mimeType);
354         }
355 
356         int width = 0;
357         int height = 0;
358 
359         if (image instanceof ToolkitImage) {
360             ImageRepresentation ir = ((ToolkitImage)image).getImageRep();
361             ir.reconstruct(ImageObserver.ALLBITS);
362             width = ir.getWidth();
363             height = ir.getHeight();
364         } else {
365             width = image.getWidth(null);
366             height = image.getHeight(null);
367         }
368 
369         // Fix for 4919639.
370         // Some Windows native applications (e.g. clipbrd.exe) do not handle
371         // 32-bpp DIBs correctly.
372         // As a workaround we switched to 24-bpp DIBs.
373         // MSDN prescribes that the bitmap array for a 24-bpp should consist of
374         // 3-byte triplets representing blue, green and red components of a
375         // pixel respectively. Additionally each scan line must be padded with
376         // zeroes to end on a LONG data-type boundary. LONG is always 32-bit.
377         // We render the given Image to a BufferedImage of type TYPE_3BYTE_BGR
378         // with non-default scanline stride and pass the resulting data buffer
379         // to the native code to fill the BITMAPINFO structure.
380         int mod = (width * 3) % 4;
381         int pad = mod > 0 ? 4 - mod : 0;
382 
383         ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
384         int[] nBits = {8, 8, 8};
385         int[] bOffs = {2, 1, 0};
386         ColorModel colorModel =
387                 new ComponentColorModel(cs, nBits, false, false,
388                         Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
389         WritableRaster raster =
390                 Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height,
391                         width * 3 + pad, 3, bOffs, null);
392 
393         BufferedImage bimage = new BufferedImage(colorModel, raster, false, null);
394 
395         // Some Windows native applications (e.g. clipbrd.exe) do not understand
396         // top-down DIBs.
397         // So we flip the image vertically and create a bottom-up DIB.
398         AffineTransform imageFlipTransform =
399                 new AffineTransform(1, 0, 0, -1, 0, height);
400 
401         Graphics2D g2d = bimage.createGraphics();
402 
403         try {
404             g2d.drawImage(image, imageFlipTransform, null);
405         } finally {
406             g2d.dispose();
407         }
408 
409         DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer();
410 
411         byte[] imageData = buffer.getData();
412         return imageDataToPlatformImageBytes(imageData, width, height, format);
413     }
414 
415     private static final byte [] UNICODE_NULL_TERMINATOR =  new byte [] {0,0};
416 
417     @Override
convertFileListToBytes(ArrayList<String> fileList)418     protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList)
419             throws IOException
420     {
421         ByteArrayOutputStream bos = new ByteArrayOutputStream();
422 
423         if(fileList.isEmpty()) {
424             //store empty unicode string (null terminator)
425             bos.write(UNICODE_NULL_TERMINATOR);
426         } else {
427             for (int i = 0; i < fileList.size(); i++) {
428                 byte[] bytes = fileList.get(i).getBytes(getDefaultUnicodeEncoding());
429                 //store unicode string with null terminator
430                 bos.write(bytes, 0, bytes.length);
431                 bos.write(UNICODE_NULL_TERMINATOR);
432             }
433         }
434 
435         // According to MSDN the byte array have to be double NULL-terminated.
436         // The array contains Unicode characters, so each NULL-terminator is
437         // a pair of bytes
438 
439         bos.write(UNICODE_NULL_TERMINATOR);
440         return bos;
441     }
442 
443     /**
444      * Returns a byte array which contains data special for the given format
445      * and for the given image data.
446      */
imageDataToPlatformImageBytes(byte[] imageData, int width, int height, long format)447     private native byte[] imageDataToPlatformImageBytes(byte[] imageData,
448                                                         int width, int height,
449                                                         long format);
450 
451     /**
452      * Translates either a byte array or an input stream which contain
453      * platform-specific image data in the given format into an Image.
454      */
455     @Override
platformImageBytesToImage(byte[] bytes, long format)456     protected Image platformImageBytesToImage(byte[] bytes, long format)
457             throws IOException {
458         String mimeType = null;
459         if (format == CF_PNG) {
460             mimeType = "image/png";
461         } else if (format == CF_JFIF) {
462             mimeType = "image/jpeg";
463         }
464         if (mimeType != null) {
465             return standardImageBytesToImage(bytes, mimeType);
466         }
467 
468         int[] imageData = platformImageBytesToImageData(bytes, format);
469         if (imageData == null) {
470             throw new IOException("data translation failed");
471         }
472 
473         int len = imageData.length - 2;
474         int width = imageData[len];
475         int height = imageData[len + 1];
476 
477         DataBufferInt buffer = new DataBufferInt(imageData, len);
478         WritableRaster raster = Raster.createPackedRaster(buffer, width,
479                 height, width,
480                 bandmasks, null);
481 
482         return new BufferedImage(directColorModel, raster, false, null);
483     }
484 
485     /**
486      * Translates a byte array which contains platform-specific image data in
487      * the given format into an integer array which contains pixel values in
488      * ARGB format. The two last elements in the array specify width and
489      * height of the image respectively.
490      */
platformImageBytesToImageData(byte[] bytes, long format)491     private native int[] platformImageBytesToImageData(byte[] bytes,
492                                                        long format)
493             throws IOException;
494 
495     @Override
dragQueryFile(byte[] bytes)496     protected native String[] dragQueryFile(byte[] bytes);
497 }
498 
499 final class WToolkitThreadBlockedHandler extends Mutex
500         implements ToolkitThreadBlockedHandler {
501 
502     @Override
enter()503     public void enter() {
504         if (!isOwned()) {
505             throw new IllegalMonitorStateException();
506         }
507         unlock();
508         startSecondaryEventLoop();
509         lock();
510     }
511 
512     @Override
exit()513     public void exit() {
514         if (!isOwned()) {
515             throw new IllegalMonitorStateException();
516         }
517         WToolkit.quitSecondaryEventLoop();
518     }
519 
startSecondaryEventLoop()520     private native void startSecondaryEventLoop();
521 }
522 
523 enum EHTMLReadMode {
524     HTML_READ_ALL,
525     HTML_READ_FRAGMENT,
526     HTML_READ_SELECTION;
527 
getEHTMLReadMode(DataFlavor df)528     public static EHTMLReadMode getEHTMLReadMode (DataFlavor df) {
529 
530         EHTMLReadMode mode = HTML_READ_SELECTION;
531 
532         String parameter = df.getParameter("document");
533 
534         if ("all".equals(parameter)) {
535             mode = HTML_READ_ALL;
536         } else if ("fragment".equals(parameter)) {
537             mode = HTML_READ_FRAGMENT;
538         }
539 
540         return mode;
541     }
542 }
543 
544 /**
545  * on decode: This stream takes an InputStream which provides data in CF_HTML format,
546  * strips off the description and context to extract the original HTML data.
547  *
548  * on encode: static convertToHTMLFormat is responsible for HTML clipboard header creation
549  */
550 class HTMLCodec extends InputStream {
551     //static section
552     public static final String ENCODING = "UTF-8";
553 
554     public static final String VERSION = "Version:";
555     public static final String START_HTML = "StartHTML:";
556     public static final String END_HTML = "EndHTML:";
557     public static final String START_FRAGMENT = "StartFragment:";
558     public static final String END_FRAGMENT = "EndFragment:";
559     public static final String START_SELECTION = "StartSelection:"; //optional
560     public static final String END_SELECTION = "EndSelection:"; //optional
561 
562     public static final String START_FRAGMENT_CMT = "<!--StartFragment-->";
563     public static final String END_FRAGMENT_CMT = "<!--EndFragment-->";
564     public static final String SOURCE_URL = "SourceURL:";
565     public static final String DEF_SOURCE_URL = "about:blank";
566 
567     public static final String EOLN = "\r\n";
568 
569     private static final String VERSION_NUM = "1.0";
570     private static final int PADDED_WIDTH = 10;
571 
toPaddedString(int n, int width)572     private static String toPaddedString(int n, int width) {
573         String string = "" + n;
574         int len = string.length();
575         if (n >= 0 && len < width) {
576             char[] array = new char[width - len];
577             Arrays.fill(array, '0');
578             StringBuffer buffer = new StringBuffer(width);
579             buffer.append(array);
580             buffer.append(string);
581             string = buffer.toString();
582         }
583         return string;
584     }
585 
586     /**
587      * convertToHTMLFormat adds the MS HTML clipboard header to byte array that
588      * contains the parameters pairs.
589      *
590      * The consequence of parameters is fixed, but some or all of them could be
591      * omitted. One parameter per one text line.
592      * It looks like that:
593      *
594      * Version:1.0\r\n                -- current supported version
595      * StartHTML:000000192\r\n        -- shift in array to the first byte after the header
596      * EndHTML:000000757\r\n          -- shift in array of last byte for HTML syntax analysis
597      * StartFragment:000000396\r\n    -- shift in array jast after <!--StartFragment-->
598      * EndFragment:000000694\r\n      -- shift in array before start  <!--EndFragment-->
599      * StartSelection:000000398\r\n   -- shift in array of the first char in copied selection
600      * EndSelection:000000692\r\n     -- shift in array of the last char in copied selection
601      * SourceURL:http://sun.com/\r\n  -- base URL for related referenses
602      * <HTML>...<BODY>...<!--StartFragment-->.....................<!--EndFragment-->...</BODY><HTML>
603      * ^                                     ^ ^                ^^                                 ^
604      * \ StartHTML                           | \-StartSelection | \EndFragment              EndHTML/
605      *                                       \-StartFragment    \EndSelection
606      *
607      *Combinations with tags sequence
608      *<!--StartFragment--><HTML>...<BODY>...</BODY><HTML><!--EndFragment-->
609      * or
610      *<HTML>...<!--StartFragment-->...<BODY>...</BODY><!--EndFragment--><HTML>
611      * are vailid too.
612      */
convertToHTMLFormat(byte[] bytes)613     public static byte[] convertToHTMLFormat(byte[] bytes) {
614         // Calculate section offsets
615         String htmlPrefix = "";
616         String htmlSuffix = "";
617         {
618             //we have extend the fragment to full HTML document correctly
619             //to avoid HTML and BODY tags doubling
620             String stContext = new String(bytes);
621             String stUpContext = stContext.toUpperCase();
622             if( -1 == stUpContext.indexOf("<HTML") ) {
623                 htmlPrefix = "<HTML>";
624                 htmlSuffix = "</HTML>";
625                 if( -1 == stUpContext.indexOf("<BODY") ) {
626                     htmlPrefix = htmlPrefix +"<BODY>";
627                     htmlSuffix = "</BODY>" + htmlSuffix;
628                 };
629             };
630         }
631 
632         String stBaseUrl = DEF_SOURCE_URL;
633         int nStartHTML =
634                 VERSION.length() + VERSION_NUM.length() + EOLN.length()
635                         + START_HTML.length() + PADDED_WIDTH + EOLN.length()
636                         + END_HTML.length() + PADDED_WIDTH + EOLN.length()
637                         + START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length()
638                         + END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length()
639                         + SOURCE_URL.length() + stBaseUrl.length() + EOLN.length()
640                 ;
641         int nStartFragment = nStartHTML + htmlPrefix.length();
642         int nEndFragment = nStartFragment + bytes.length - 1;
643         int nEndHTML = nEndFragment + htmlSuffix.length();
644 
645         StringBuilder header = new StringBuilder(
646                 nStartFragment
647                         + START_FRAGMENT_CMT.length()
648         );
649         //header
650         header.append(VERSION);
651         header.append(VERSION_NUM);
652         header.append(EOLN);
653 
654         header.append(START_HTML);
655         header.append(toPaddedString(nStartHTML, PADDED_WIDTH));
656         header.append(EOLN);
657 
658         header.append(END_HTML);
659         header.append(toPaddedString(nEndHTML, PADDED_WIDTH));
660         header.append(EOLN);
661 
662         header.append(START_FRAGMENT);
663         header.append(toPaddedString(nStartFragment, PADDED_WIDTH));
664         header.append(EOLN);
665 
666         header.append(END_FRAGMENT);
667         header.append(toPaddedString(nEndFragment, PADDED_WIDTH));
668         header.append(EOLN);
669 
670         header.append(SOURCE_URL);
671         header.append(stBaseUrl);
672         header.append(EOLN);
673 
674         //HTML
675         header.append(htmlPrefix);
676 
677         byte[] headerBytes = null, trailerBytes = null;
678 
679         try {
680             headerBytes = header.toString().getBytes(ENCODING);
681             trailerBytes = htmlSuffix.getBytes(ENCODING);
682         } catch (UnsupportedEncodingException cannotHappen) {
683         }
684 
685         byte[] retval = new byte[headerBytes.length + bytes.length +
686                 trailerBytes.length];
687 
688         System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length);
689         System.arraycopy(bytes, 0, retval, headerBytes.length,
690                 bytes.length - 1);
691         System.arraycopy(trailerBytes, 0, retval,
692                 headerBytes.length + bytes.length - 1,
693                 trailerBytes.length);
694         retval[retval.length-1] = 0;
695 
696         return retval;
697     }
698 
699     ////////////////////////////////////
700     //decoder instance data and methods:
701 
702     private final BufferedInputStream bufferedStream;
703     private boolean descriptionParsed = false;
704     private boolean closed = false;
705 
706     // InputStreamReader uses an 8K buffer. The size is not customizable.
707     public static final int BYTE_BUFFER_LEN = 8192;
708 
709     // CharToByteUTF8.getMaxBytesPerChar returns 3, so we should not buffer
710     // more chars than 3 times the number of bytes we can buffer.
711     public static final int CHAR_BUFFER_LEN = BYTE_BUFFER_LEN / 3;
712 
713     private static final String FAILURE_MSG =
714             "Unable to parse HTML description: ";
715     private static final String INVALID_MSG =
716             " invalid";
717 
718     //HTML header mapping:
719     private long   iHTMLStart,// StartHTML -- shift in array to the first byte after the header
720             iHTMLEnd,  // EndHTML -- shift in array of last byte for HTML syntax analysis
721             iFragStart,// StartFragment -- shift in array jast after <!--StartFragment-->
722             iFragEnd,  // EndFragment -- shift in array before start <!--EndFragment-->
723             iSelStart, // StartSelection -- shift in array of the first char in copied selection
724             iSelEnd;   // EndSelection -- shift in array of the last char in copied selection
725     private String stBaseURL; // SourceURL -- base URL for related referenses
726     private String stVersion; // Version -- current supported version
727 
728     //Stream reader markers:
729     private long iStartOffset,
730             iEndOffset,
731             iReadCount;
732 
733     private EHTMLReadMode readMode;
734 
HTMLCodec( InputStream _bytestream, EHTMLReadMode _readMode)735     public HTMLCodec(
736             InputStream _bytestream,
737             EHTMLReadMode _readMode) throws IOException
738     {
739         bufferedStream = new BufferedInputStream(_bytestream, BYTE_BUFFER_LEN);
740         readMode = _readMode;
741     }
742 
getBaseURL()743     public synchronized String getBaseURL() throws IOException
744     {
745         if( !descriptionParsed ) {
746             parseDescription();
747         }
748         return stBaseURL;
749     }
getVersion()750     public synchronized String getVersion() throws IOException
751     {
752         if( !descriptionParsed ) {
753             parseDescription();
754         }
755         return stVersion;
756     }
757 
758     /**
759      * parseDescription parsing HTML clipboard header as it described in
760      * comment to convertToHTMLFormat
761      */
parseDescription()762     private void parseDescription() throws IOException
763     {
764         stBaseURL = null;
765         stVersion = null;
766 
767         // initialization of array offset pointers
768         // to the same "uninitialized" state.
769         iHTMLEnd =
770                 iHTMLStart =
771                         iFragEnd =
772                                 iFragStart =
773                                         iSelEnd =
774                                                 iSelStart = -1;
775 
776         bufferedStream.mark(BYTE_BUFFER_LEN);
777         String astEntries[] = new String[] {
778                 //common
779                 VERSION,
780                 START_HTML,
781                 END_HTML,
782                 START_FRAGMENT,
783                 END_FRAGMENT,
784                 //ver 1.0
785                 START_SELECTION,
786                 END_SELECTION,
787                 SOURCE_URL
788         };
789         BufferedReader bufferedReader = new BufferedReader(
790                 new InputStreamReader(
791                         bufferedStream,
792                         ENCODING
793                 ),
794                 CHAR_BUFFER_LEN
795         );
796         long iHeadSize = 0;
797         long iCRSize = EOLN.length();
798         int iEntCount = astEntries.length;
799         boolean bContinue = true;
800 
801         for( int  iEntry = 0; iEntry < iEntCount; ++iEntry ){
802             String stLine = bufferedReader.readLine();
803             if( null==stLine ) {
804                 break;
805             }
806             //some header entries are optional, but the order is fixed.
807             for( ; iEntry < iEntCount; ++iEntry ){
808                 if( !stLine.startsWith(astEntries[iEntry]) ) {
809                     continue;
810                 }
811                 iHeadSize += stLine.length() + iCRSize;
812                 String stValue = stLine.substring(astEntries[iEntry].length()).trim();
813                 if( null!=stValue ) {
814                     try{
815                         switch( iEntry ){
816                             case 0:
817                                 stVersion = stValue;
818                                 break;
819                             case 1:
820                                 iHTMLStart = Integer.parseInt(stValue);
821                                 break;
822                             case 2:
823                                 iHTMLEnd = Integer.parseInt(stValue);
824                                 break;
825                             case 3:
826                                 iFragStart = Integer.parseInt(stValue);
827                                 break;
828                             case 4:
829                                 iFragEnd = Integer.parseInt(stValue);
830                                 break;
831                             case 5:
832                                 iSelStart = Integer.parseInt(stValue);
833                                 break;
834                             case 6:
835                                 iSelEnd = Integer.parseInt(stValue);
836                                 break;
837                             case 7:
838                                 stBaseURL = stValue;
839                                 break;
840                         };
841                     } catch ( NumberFormatException e ) {
842                         throw new IOException(FAILURE_MSG + astEntries[iEntry]+ " value " + e + INVALID_MSG);
843                     }
844                 }
845                 break;
846             }
847         }
848         //some entries could absent in HTML header,
849         //so we have find they by another way.
850         if( -1 == iHTMLStart )
851             iHTMLStart = iHeadSize;
852         if( -1 == iFragStart )
853             iFragStart = iHTMLStart;
854         if( -1 == iFragEnd )
855             iFragEnd = iHTMLEnd;
856         if( -1 == iSelStart )
857             iSelStart = iFragStart;
858         if( -1 == iSelEnd )
859             iSelEnd = iFragEnd;
860 
861         //one of possible modes
862         switch( readMode ){
863             case HTML_READ_ALL:
864                 iStartOffset = iHTMLStart;
865                 iEndOffset = iHTMLEnd;
866                 break;
867             case HTML_READ_FRAGMENT:
868                 iStartOffset = iFragStart;
869                 iEndOffset = iFragEnd;
870                 break;
871             case HTML_READ_SELECTION:
872             default:
873                 iStartOffset = iSelStart;
874                 iEndOffset = iSelEnd;
875                 break;
876         }
877 
878         bufferedStream.reset();
879         if( -1 == iStartOffset ){
880             throw new IOException(FAILURE_MSG + "invalid HTML format.");
881         }
882 
883         int curOffset = 0;
884         while (curOffset < iStartOffset){
885             curOffset += bufferedStream.skip(iStartOffset - curOffset);
886         }
887 
888         iReadCount = curOffset;
889 
890         if( iStartOffset != iReadCount ){
891             throw new IOException(FAILURE_MSG + "Byte stream ends in description.");
892         }
893         descriptionParsed = true;
894     }
895 
896     @Override
read()897     public synchronized int read() throws IOException {
898         if( closed ){
899             throw new IOException("Stream closed");
900         }
901 
902         if( !descriptionParsed ){
903             parseDescription();
904         }
905         if( -1 != iEndOffset && iReadCount >= iEndOffset ) {
906             return -1;
907         }
908 
909         int retval = bufferedStream.read();
910         if( retval == -1 ) {
911             return -1;
912         }
913         ++iReadCount;
914         return retval;
915     }
916 
917     @Override
close()918     public synchronized void close() throws IOException {
919         if( !closed ){
920             closed = true;
921             bufferedStream.close();
922         }
923     }
924 }
925