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