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