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