1 /*
   2  * Copyright (c) 2011, 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.lwawt.macosx;
  27 
  28 import java.awt.*;
  29 import java.awt.image.*;
  30 import sun.awt.image.ImageRepresentation;
  31 
  32 import java.io.*;
  33 import java.net.URL;
  34 import java.text.Normalizer;
  35 import java.text.Normalizer.Form;
  36 import java.util.*;
  37 
  38 import java.awt.datatransfer.*;
  39 import sun.awt.datatransfer.*;
  40 
  41 public class CDataTransferer extends DataTransferer {
  42     private static final Map<String, Long> predefinedClipboardNameMap;
  43     private static final Map<Long, String> predefinedClipboardFormatMap;
  44 
  45     // See SystemFlavorMap, or the flavormap.properties file:
  46     // We should define a few more types in flavormap.properties, it's rather slim now.
  47     private static final String[] predefinedClipboardNames = {
  48         "",
  49         "STRING",
  50         "FILE_NAME",
  51         "TIFF",
  52         "RICH_TEXT",
  53         "HTML",
  54         "PDF",
  55         "URL"
  56     };
  57 
  58     static {
  59         Map<String, Long> nameMap = new HashMap<String, Long>(predefinedClipboardNames.length, 1.0f);
  60         Map<Long, String> formatMap = new HashMap<Long, String>(predefinedClipboardNames.length, 1.0f);
  61         for (int i = 1; i < predefinedClipboardNames.length; i++) {
  62             nameMap.put(predefinedClipboardNames[i], new Long(i));
  63             formatMap.put(new Long(i), predefinedClipboardNames[i]);
  64         }
  65         predefinedClipboardNameMap = Collections.synchronizedMap(nameMap);
  66         predefinedClipboardFormatMap = Collections.synchronizedMap(formatMap);
  67     }
  68 
  69     public static final int CF_UNSUPPORTED = 0;
  70     public static final int CF_STRING      = 1;
  71     public static final int CF_FILE        = 2;
  72     public static final int CF_TIFF        = 3;
  73     public static final int CF_RICH_TEXT   = 4;
  74     public static final int CF_HTML        = 5;
  75     public static final int CF_PDF         = 6;
  76     public static final int CF_URL         = 7;
  77     public static final int CF_PNG         = 10;
  78     public static final int CF_JPEG        = 11;
  79 
  80     public static final Long L_CF_TIFF = predefinedClipboardNameMap.get(predefinedClipboardNames[CF_TIFF]);
  81 
  82     // Image file formats with java.awt.Image representation:
  83     private static final Long[] imageFormats = new Long[] {
  84         L_CF_TIFF
  85     };
  86 
  87 
  88     private CDataTransferer() {}
  89 
  90     private static CDataTransferer fTransferer;
  91 
  92     public static synchronized CDataTransferer getInstanceImpl() {
  93         if (fTransferer == null) {
  94             fTransferer = new CDataTransferer();
  95         }
  96 
  97         return fTransferer;
  98     }
  99 
 100     public String getDefaultUnicodeEncoding() {
 101         return "utf-16le";
 102     }
 103 
 104     public boolean isLocaleDependentTextFormat(long format) {
 105         return format == CF_STRING;
 106     }
 107 
 108     public boolean isFileFormat(long format) {
 109         return format == CF_FILE;
 110     }
 111 
 112     public boolean isImageFormat(long format) {
 113         int ifmt = (int)format;
 114         switch(ifmt) {
 115             case CF_TIFF:
 116             case CF_PDF:
 117             case CF_PNG:
 118             case CF_JPEG:
 119                 return true;
 120             default:
 121                 return false;
 122         }
 123     }
 124 
 125     protected Long[] getImageFormatsAsLongArray() {
 126         return imageFormats;
 127     }
 128 
 129     public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException
 130         {
 131             byte[] bytes = super.translateTransferable(contents, flavor, format);
 132 
 133             // 9-12-02 VL: we may need to do something like Windows here.
 134             //if (format == CF_HTML) {
 135             //    bytes = HTMLSupport.convertToHTMLFormat(bytes);
 136             //}
 137 
 138             return bytes;
 139         }
 140 
 141     protected Object translateBytesOrStream(InputStream stream, byte[] bytes, DataFlavor flavor, long format,
 142                                             Transferable transferable) throws IOException
 143         {
 144             // 5-28-03 VL: [Radar 3266030]
 145             // We need to do like Windows does here.
 146             if (format == CF_HTML && flavor.isFlavorTextType()) {
 147                 if (stream == null) {
 148                     stream = new ByteArrayInputStream(bytes);
 149                     bytes = null;
 150                 }
 151 
 152                 stream = new HTMLDecodingInputStream(stream);
 153             }
 154 
 155             if (format == CF_URL && URL.class.equals(flavor.getRepresentationClass()))
 156             {
 157                 if (bytes == null) {
 158                     bytes = inputStreamToByteArray(stream);
 159                     stream = null;
 160                 }
 161 
 162                 String charset = getDefaultTextCharset();
 163                 if (transferable != null && transferable.isDataFlavorSupported(javaTextEncodingFlavor)) {
 164                     try {
 165                         charset = new String((byte[])transferable.getTransferData(javaTextEncodingFlavor), "UTF-8");
 166                     } catch (UnsupportedFlavorException cannotHappen) {
 167                     }
 168                 }
 169 
 170                 return new URL(new String(bytes, charset));
 171             }
 172 
 173             if (format == CF_STRING) {
 174                 bytes = Normalizer.normalize(new String(bytes, "UTF8"), Form.NFC).getBytes("UTF8");
 175             }
 176 
 177             return super.translateBytesOrStream(stream, bytes, flavor, format, transferable);
 178         }
 179 
 180 
 181     synchronized protected Long getFormatForNativeAsLong(String str) {
 182         Long format = predefinedClipboardNameMap.get(str);
 183 
 184         if (format == null) {
 185             if (java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance()) {
 186                 // Do not try to access GUI manager for unknown format
 187                 return new Long(-1);
 188             }
 189             format = new Long(registerFormatWithPasteboard(str));
 190             predefinedClipboardNameMap.put(str, format);
 191             predefinedClipboardFormatMap.put(format, str);
 192         }
 193 
 194         return format;
 195     }
 196 
 197     /*
 198      * Adds type to native mapping NSDictionary.
 199      */
 200     private native long registerFormatWithPasteboard(String type);
 201 
 202     // Get registered native format string for an index, return null if unknown:
 203     private native String formatForIndex(long index);
 204 
 205     protected String getNativeForFormat(long format) {
 206         String returnValue = null;
 207 
 208         // The most common case - just index the array of predefined names:
 209         if (format >= 0 && format < predefinedClipboardNames.length) {
 210             returnValue = predefinedClipboardNames[(int) format];
 211         } else {
 212             Long formatObj = new Long(format);
 213             returnValue = predefinedClipboardFormatMap.get(formatObj);
 214 
 215             // predefinedClipboardFormatMap may not know this format:
 216             if (returnValue == null) {
 217                 returnValue = formatForIndex(format);
 218 
 219                 // Native clipboard may not know this format either:
 220                 if (returnValue != null) {
 221                     predefinedClipboardNameMap.put(returnValue, formatObj);
 222                     predefinedClipboardFormatMap.put(formatObj, returnValue);
 223                 }
 224             }
 225         }
 226 
 227         if (returnValue == null) {
 228             returnValue = predefinedClipboardNames[CF_UNSUPPORTED];
 229         }
 230 
 231         return returnValue;
 232     }
 233 
 234     private final ToolkitThreadBlockedHandler handler = new CToolkitThreadBlockedHandler();
 235 
 236     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
 237         return handler;
 238     }
 239 
 240     protected byte[] imageToPlatformBytes(Image image, long format) {
 241         int w = image.getWidth(null);
 242         int h = image.getHeight(null);
 243         BufferedImage bimage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
 244         Graphics g = bimage.getGraphics();
 245         g.drawImage(image, 0, 0, w, h, null);
 246         g.dispose();
 247         Raster raster = bimage.getRaster();
 248         DataBuffer buffer = raster.getDataBuffer();
 249         return imageDataToPlatformImageBytes(((DataBufferInt)buffer).getData(),
 250                                              raster.getWidth(),
 251                                              raster.getHeight());
 252     }
 253 
 254     private static native String[] nativeDragQueryFile(final byte[] bytes);
 255     protected String[] dragQueryFile(final byte[] bytes) {
 256         if (bytes == null) return null;
 257         if (new String(bytes).startsWith("Unsupported type")) return null;
 258         return nativeDragQueryFile(bytes);
 259     }
 260 
 261     private native byte[] imageDataToPlatformImageBytes(int[] rData, int nW, int nH);
 262 
 263     /**
 264         * Translates either a byte array or an input stream which contain
 265      * platform-specific image data in the given format into an Image.
 266      */
 267     protected Image platformImageBytesOrStreamToImage(InputStream stream, byte[] bytes, long format) throws IOException {
 268         byte[] imageData = bytes;
 269 
 270         if (imageData == null)
 271             imageData = inputStreamToByteArray(stream);
 272 
 273         return getImageForByteStream(imageData);
 274     }
 275 
 276     private native Image getImageForByteStream(byte[] bytes);
 277 
 278     @Override
 279     protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList) throws IOException {
 280         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 281         for (int i = 0; i < fileList.size(); i++)
 282         {
 283             byte[] bytes = fileList.get(i).getBytes();
 284             bos.write(bytes, 0, bytes.length);
 285             bos.write(0);
 286         }
 287         return bos;
 288     }
 289 
 290     @Override
 291     protected boolean isURIListFormat(long format) {
 292         String nat = getNativeForFormat(format);
 293         if (nat == null) {
 294             return false;
 295         }
 296         try {
 297             DataFlavor df = new DataFlavor(nat);
 298             if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) {
 299                 return true;
 300             }
 301         } catch (Exception e) {
 302             // Not a MIME format.
 303         }
 304         return false;
 305     }
 306 }
 307 
 308 
 309 // ---- Code borrowed from WDataTransferer: ----
 310 // This will come handy for supporting HTML data.
 311 
 312 final class HTMLSupport {
 313     public static final String ENCODING = "UTF-8";
 314 
 315     public static final String VERSION = "Version:";
 316     public static final String START_HTML = "StartHTML:";
 317     public static final String END_HTML = "EndHTML:";
 318     public static final String START_FRAGMENT = "StartFragment:";
 319     public static final String END_FRAGMENT = "EndFragment:";
 320     public static final String START_FRAGMENT_CMT = "<!--StartFragment-->";
 321     public static final String END_FRAGMENT_CMT = "<!--EndFragment-->";
 322     public static final String EOLN = "\r\n";
 323 
 324     private static final String VERSION_NUM = "0.9";
 325     private static final String HTML_START_END = "-1";
 326 
 327     private static final int PADDED_WIDTH = 10;
 328 
 329     private static final int HEADER_LEN =
 330         VERSION.length() + VERSION_NUM.length() + EOLN.length() +
 331         START_HTML.length() + HTML_START_END.length() + EOLN.length() +
 332         END_HTML.length() + HTML_START_END.length() + EOLN.length() +
 333         START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 334         END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 335         START_FRAGMENT_CMT.length() + EOLN.length();
 336     private static final String HEADER_LEN_STR =
 337         toPaddedString(HEADER_LEN, PADDED_WIDTH);
 338 
 339     private static final String TRAILER = END_FRAGMENT_CMT + EOLN + '\0';
 340 
 341     private static String toPaddedString(int n, int width) {
 342         String string = "" + n;
 343         int len = string.length();
 344         if (n >= 0 && len < width) {
 345             char[] array = new char[width - len];
 346             Arrays.fill(array, '0');
 347             StringBuffer buffer = new StringBuffer();
 348             buffer.append(array);
 349             buffer.append(string);
 350             string = buffer.toString();
 351         }
 352         return string;
 353     }
 354 
 355     public static byte[] convertToHTMLFormat(byte[] bytes) {
 356         StringBuffer header = new StringBuffer(HEADER_LEN);
 357         header.append(VERSION);
 358         header.append(VERSION_NUM);
 359         header.append(EOLN);
 360         header.append(START_HTML);
 361         header.append(HTML_START_END);
 362         header.append(EOLN);
 363         header.append(END_HTML);
 364         header.append(HTML_START_END);
 365         header.append(EOLN);
 366         header.append(START_FRAGMENT);
 367         header.append(HEADER_LEN_STR);
 368         header.append(EOLN);
 369         header.append(END_FRAGMENT);
 370         // Strip terminating NUL byte from array
 371         header.append(toPaddedString(HEADER_LEN + bytes.length - 1,
 372                                      PADDED_WIDTH));
 373         header.append(EOLN);
 374         header.append(START_FRAGMENT_CMT);
 375         header.append(EOLN);
 376 
 377         byte[] headerBytes = null, trailerBytes = null;
 378 
 379         try {
 380             headerBytes = new String(header).getBytes(ENCODING);
 381             trailerBytes = TRAILER.getBytes(ENCODING);
 382         } catch (UnsupportedEncodingException cannotHappen) {
 383         }
 384 
 385         byte[] retval = new byte[headerBytes.length + bytes.length - 1 +
 386             trailerBytes.length];
 387 
 388         System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length);
 389         System.arraycopy(bytes, 0, retval, headerBytes.length,
 390                          bytes.length - 1);
 391         System.arraycopy(trailerBytes, 0, retval,
 392                          headerBytes.length + bytes.length - 1,
 393                          trailerBytes.length);
 394 
 395         return retval;
 396     }
 397 }
 398 
 399 /**
 400 * This stream takes an InputStream which provides data in CF_HTML format,
 401  * strips off the description and context to extract the original HTML data.
 402  */
 403 class HTMLDecodingInputStream extends InputStream {
 404 
 405     private final BufferedInputStream bufferedStream;
 406     private boolean descriptionParsed = false;
 407     private boolean closed = false;
 408     private int index;
 409     private int end;
 410 
 411     // InputStreamReader uses an 8K buffer. The size is not customizable.
 412     public static final int BYTE_BUFFER_LEN = 8192;
 413 
 414     // CharToByteUTF8.getMaxBytesPerChar returns 3, so we should not buffer
 415     // more chars than 3 times the number of bytes we can buffer.
 416     public static final int CHAR_BUFFER_LEN = BYTE_BUFFER_LEN / 3;
 417 
 418     private static final String FAILURE_MSG =
 419         "Unable to parse HTML description: ";
 420     private static final String INVALID_MSG = " invalid";
 421 
 422     public HTMLDecodingInputStream(InputStream bytestream) throws IOException {
 423         bufferedStream = new BufferedInputStream(bytestream, BYTE_BUFFER_LEN);
 424     }
 425 
 426     private void parseDescription() throws IOException {
 427         bufferedStream.mark(BYTE_BUFFER_LEN);
 428 
 429         BufferedReader bufferedReader = new BufferedReader
 430             (new InputStreamReader(bufferedStream, HTMLSupport.ENCODING),
 431              CHAR_BUFFER_LEN);
 432         String version = bufferedReader.readLine().trim();
 433         if (version == null || !version.startsWith(HTMLSupport.VERSION)) {
 434             // Not MS-compliant HTML text. Return raw text from read().
 435             index = 0;
 436             end = -1;
 437             bufferedStream.reset();
 438             return;
 439         }
 440 
 441         String input;
 442         boolean startHTML, endHTML, startFragment, endFragment;
 443         startHTML = endHTML = startFragment = endFragment = false;
 444 
 445         try {
 446             do {
 447                 input = bufferedReader.readLine().trim();
 448                 if (input == null) {
 449                     close();
 450                     throw new IOException(FAILURE_MSG);
 451                 } else if (input.startsWith(HTMLSupport.START_HTML)) {
 452                     int val = Integer.parseInt
 453                     (input.substring(HTMLSupport.START_HTML.length(),
 454                                      input.length()).trim());
 455                     if (val >= 0) {
 456                         index = val;
 457                         startHTML = true;
 458                     } else if (val != -1) {
 459                         close();
 460                         throw new IOException(FAILURE_MSG +
 461                                               HTMLSupport.START_HTML +
 462                                               INVALID_MSG);
 463                     }
 464                 } else if (input.startsWith(HTMLSupport.END_HTML)) {
 465                     int val = Integer.parseInt
 466                     (input.substring(HTMLSupport.END_HTML.length(),
 467                                      input.length()).trim());
 468                     if (val >= 0) {
 469                         end = val;
 470                         endHTML = true;
 471                     } else if (val != -1) {
 472                         close();
 473                         throw new IOException(FAILURE_MSG +
 474                                               HTMLSupport.END_HTML +
 475                                               INVALID_MSG);
 476                     }
 477                 } else if (!startHTML && !endHTML &&
 478                            input.startsWith(HTMLSupport.START_FRAGMENT)) {
 479                     index = Integer.parseInt
 480                     (input.substring(HTMLSupport.START_FRAGMENT.length(),
 481                                      input.length()).trim());
 482                     if (index < 0) {
 483                         close();
 484                         throw new IOException(FAILURE_MSG +
 485                                               HTMLSupport.START_FRAGMENT +
 486                                               INVALID_MSG);
 487                     }
 488                     startFragment = true;
 489                 } else if (!startHTML && !endHTML &&
 490                            input.startsWith(HTMLSupport.END_FRAGMENT)) {
 491                     end = Integer.parseInt
 492                     (input.substring(HTMLSupport.END_FRAGMENT.length(),
 493                                      input.length()).trim());
 494                     if (end < 0) {
 495                         close();
 496                         throw new IOException(FAILURE_MSG +
 497                                               HTMLSupport.END_FRAGMENT +
 498                                               INVALID_MSG);
 499                     }
 500                     endFragment = true;
 501                 }
 502             } while (!((startHTML && endHTML) ||
 503                        (startFragment && endFragment)));
 504         } catch (NumberFormatException e) {
 505             close();
 506             throw new IOException(FAILURE_MSG + e);
 507         }
 508 
 509         bufferedStream.reset();
 510 
 511         for (int i = 0; i < index; i++) {
 512             if (bufferedStream.read() == -1) {
 513                 close();
 514                 throw new IOException(FAILURE_MSG +
 515                                       "Byte stream ends in description.");
 516             }
 517         }
 518     }
 519 
 520     public int read() throws IOException {
 521         if (closed) {
 522             throw new IOException("Stream closed");
 523         }
 524 
 525         if (!descriptionParsed) {
 526             parseDescription(); // initializes 'index' and 'end'
 527             descriptionParsed = true;
 528         }
 529 
 530         if (end != -1 && index >= end) {
 531             return -1;
 532         }
 533 
 534         int retval = bufferedStream.read();
 535         if (retval == -1) {
 536             index = end = 0; // so future read() calls will fail quickly
 537             return -1;
 538         }
 539 
 540         index++;
 541     //    System.out.print((char)retval);
 542         return retval;
 543     }
 544 
 545     public void close() throws IOException {
 546         if (!closed) {
 547             closed = true;
 548             bufferedStream.close();
 549         }
 550     }
 551 }