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.desktop.DataTransferer;
  71 import sun.awt.datatransfer.desktop.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++) {
 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     static synchronized WDataTransferer getInstanceImpl() {
 163         if (transferer == null) {
 164             transferer = new WDataTransferer();
 165         }
 166         return transferer;
 167     }
 168 
 169     @Override
 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
 184     public String getDefaultUnicodeEncoding() {
 185         return "utf-16le";
 186     }
 187 
 188     @Override
 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
 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
 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
 291     public boolean isLocaleDependentTextFormat(long format) {
 292         return format == CF_TEXT || format == CFSTR_INETURL;
 293     }
 294 
 295     @Override
 296     public boolean isFileFormat(long format) {
 297         return format == CF_HDROP || format == CF_FILEGROUPDESCRIPTORA || format == CF_FILEGROUPDESCRIPTORW;
 298     }
 299 
 300     @Override
 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
 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
 320     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
 321         return handler;
 322     }
 323 
 324     /**
 325      * Calls the Win32 RegisterClipboardFormat function to register
 326      * a non-standard format.
 327      */
 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      */
 334     private static native String getClipboardFormatName(long format);
 335 
 336     @Override
 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
 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
 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      */
 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
 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      */
 491     private native int[] platformImageBytesToImageData(byte[] bytes,
 492                                                        long format)
 493             throws IOException;
 494 
 495     @Override
 496     protected native String[] dragQueryFile(byte[] bytes);
 497 }
 498 
 499 final class WToolkitThreadBlockedHandler extends Mutex
 500         implements ToolkitThreadBlockedHandler {
 501 
 502     @Override
 503     public void enter() {
 504         if (!isOwned()) {
 505             throw new IllegalMonitorStateException();
 506         }
 507         unlock();
 508         startSecondaryEventLoop();
 509         lock();
 510     }
 511 
 512     @Override
 513     public void exit() {
 514         if (!isOwned()) {
 515             throw new IllegalMonitorStateException();
 516         }
 517         WToolkit.quitSecondaryEventLoop();
 518     }
 519 
 520     private native void startSecondaryEventLoop();
 521 }
 522 
 523 enum EHTMLReadMode {
 524     HTML_READ_ALL,
 525     HTML_READ_FRAGMENT,
 526     HTML_READ_SELECTION;
 527 
 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 
 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      */
 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 
 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 
 743     public synchronized String getBaseURL() throws IOException
 744     {
 745         if( !descriptionParsed ) {
 746             parseDescription();
 747         }
 748         return stBaseURL;
 749     }
 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      */
 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
 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
 918     public synchronized void close() throws IOException {
 919         if( !closed ){
 920             closed = true;
 921             bufferedStream.close();
 922         }
 923     }
 924 }