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