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             format = new Long(registerFormatWithPasteboard(str));
 186             predefinedClipboardNameMap.put(str, format);
 187             predefinedClipboardFormatMap.put(format, str);
 188         }
 189 
 190         return format;
 191     }
 192 
 193     /*
 194      * Adds type to native mapping NSDictionary.
 195      */
 196     private native long registerFormatWithPasteboard(String type);
 197 
 198     // Get registered native format string for an index, return null if unknown:
 199     private native String formatForIndex(long index);
 200 
 201     protected String getNativeForFormat(long format) {
 202         String returnValue = null;
 203 
 204         // The most common case - just index the array of predefined names:
 205         if (format >= 0 && format < predefinedClipboardNames.length) {
 206             returnValue = predefinedClipboardNames[(int) format];
 207         } else {
 208             Long formatObj = new Long(format);
 209             returnValue = predefinedClipboardFormatMap.get(formatObj);
 210 
 211             // predefinedClipboardFormatMap may not know this format:
 212             if (returnValue == null) {
 213                 returnValue = formatForIndex(format);
 214 
 215                 // Native clipboard may not know this format either:
 216                 if (returnValue != null) {
 217                     predefinedClipboardNameMap.put(returnValue, formatObj);
 218                     predefinedClipboardFormatMap.put(formatObj, returnValue);
 219                 }
 220             }
 221         }
 222 
 223         if (returnValue == null) {
 224             returnValue = predefinedClipboardNames[CF_UNSUPPORTED];
 225         }
 226 
 227         return returnValue;
 228     }
 229 
 230     private final ToolkitThreadBlockedHandler handler = new CToolkitThreadBlockedHandler();
 231 
 232     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
 233         return handler;
 234     }
 235 
 236     protected byte[] imageToPlatformBytes(Image image, long format) {
 237         int w = image.getWidth(null);
 238         int h = image.getHeight(null);
 239         BufferedImage bimage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
 240         Graphics g = bimage.getGraphics();
 241         g.drawImage(image, 0, 0, w, h, null);
 242         g.dispose();
 243         Raster raster = bimage.getRaster();
 244         DataBuffer buffer = raster.getDataBuffer();
 245         return imageDataToPlatformImageBytes(((DataBufferInt)buffer).getData(),
 246                                              raster.getWidth(),
 247                                              raster.getHeight());
 248     }
 249 
 250     private static native String[] nativeDragQueryFile(final byte[] bytes);
 251     protected String[] dragQueryFile(final byte[] bytes) {
 252         if (bytes == null) return null;
 253         if (new String(bytes).startsWith("Unsupported type")) return null;
 254         return nativeDragQueryFile(bytes);
 255     }
 256 
 257     private native byte[] imageDataToPlatformImageBytes(int[] rData, int nW, int nH);
 258 
 259     /**
 260         * Translates either a byte array or an input stream which contain
 261      * platform-specific image data in the given format into an Image.
 262      */
 263     protected Image platformImageBytesOrStreamToImage(InputStream stream, byte[] bytes, long format) throws IOException {
 264         byte[] imageData = bytes;
 265 
 266         if (imageData == null)
 267             imageData = inputStreamToByteArray(stream);
 268 
 269         return getImageForByteStream(imageData);
 270     }
 271 
 272     private native Image getImageForByteStream(byte[] bytes);
 273 
 274     @Override
 275     protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList) throws IOException {
 276         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 277         for (int i = 0; i < fileList.size(); i++)
 278         {
 279             byte[] bytes = fileList.get(i).getBytes();
 280             bos.write(bytes, 0, bytes.length);
 281             bos.write(0);
 282         }
 283         return bos;
 284     }
 285 
 286     @Override
 287     protected boolean isURIListFormat(long format) {
 288         String nat = getNativeForFormat(format);
 289         if (nat == null) {
 290             return false;
 291         }
 292         try {
 293             DataFlavor df = new DataFlavor(nat);
 294             if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) {
 295                 return true;
 296             }
 297         } catch (Exception e) {
 298             // Not a MIME format.
 299         }
 300         return false;
 301     }
 302 }
 303 
 304 
 305 // ---- Code borrowed from WDataTransferer: ----
 306 // This will come handy for supporting HTML data.
 307 
 308 final class HTMLSupport {
 309     public static final String ENCODING = "UTF-8";
 310 
 311     public static final String VERSION = "Version:";
 312     public static final String START_HTML = "StartHTML:";
 313     public static final String END_HTML = "EndHTML:";
 314     public static final String START_FRAGMENT = "StartFragment:";
 315     public static final String END_FRAGMENT = "EndFragment:";
 316     public static final String START_FRAGMENT_CMT = "<!--StartFragment-->";
 317     public static final String END_FRAGMENT_CMT = "<!--EndFragment-->";
 318     public static final String EOLN = "\r\n";
 319 
 320     private static final String VERSION_NUM = "0.9";
 321     private static final String HTML_START_END = "-1";
 322 
 323     private static final int PADDED_WIDTH = 10;
 324 
 325     private static final int HEADER_LEN =
 326         VERSION.length() + VERSION_NUM.length() + EOLN.length() +
 327         START_HTML.length() + HTML_START_END.length() + EOLN.length() +
 328         END_HTML.length() + HTML_START_END.length() + EOLN.length() +
 329         START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 330         END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 331         START_FRAGMENT_CMT.length() + EOLN.length();
 332     private static final String HEADER_LEN_STR =
 333         toPaddedString(HEADER_LEN, PADDED_WIDTH);
 334 
 335     private static final String TRAILER = END_FRAGMENT_CMT + EOLN + '\0';
 336 
 337     private static String toPaddedString(int n, int width) {
 338         String string = "" + n;
 339         int len = string.length();
 340         if (n >= 0 && len < width) {
 341             char[] array = new char[width - len];
 342             Arrays.fill(array, '0');
 343             StringBuffer buffer = new StringBuffer();
 344             buffer.append(array);
 345             buffer.append(string);
 346             string = buffer.toString();
 347         }
 348         return string;
 349     }
 350 
 351     public static byte[] convertToHTMLFormat(byte[] bytes) {
 352         StringBuffer header = new StringBuffer(HEADER_LEN);
 353         header.append(VERSION);
 354         header.append(VERSION_NUM);
 355         header.append(EOLN);
 356         header.append(START_HTML);
 357         header.append(HTML_START_END);
 358         header.append(EOLN);
 359         header.append(END_HTML);
 360         header.append(HTML_START_END);
 361         header.append(EOLN);
 362         header.append(START_FRAGMENT);
 363         header.append(HEADER_LEN_STR);
 364         header.append(EOLN);
 365         header.append(END_FRAGMENT);
 366         // Strip terminating NUL byte from array
 367         header.append(toPaddedString(HEADER_LEN + bytes.length - 1,
 368                                      PADDED_WIDTH));
 369         header.append(EOLN);
 370         header.append(START_FRAGMENT_CMT);
 371         header.append(EOLN);
 372 
 373         byte[] headerBytes = null, trailerBytes = null;
 374 
 375         try {
 376             headerBytes = new String(header).getBytes(ENCODING);
 377             trailerBytes = TRAILER.getBytes(ENCODING);
 378         } catch (UnsupportedEncodingException cannotHappen) {
 379         }
 380 
 381         byte[] retval = new byte[headerBytes.length + bytes.length - 1 +
 382             trailerBytes.length];
 383 
 384         System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length);
 385         System.arraycopy(bytes, 0, retval, headerBytes.length,
 386                          bytes.length - 1);
 387         System.arraycopy(trailerBytes, 0, retval,
 388                          headerBytes.length + bytes.length - 1,
 389                          trailerBytes.length);
 390 
 391         return retval;
 392     }
 393 }
 394 
 395 /**
 396 * This stream takes an InputStream which provides data in CF_HTML format,
 397  * strips off the description and context to extract the original HTML data.
 398  */
 399 class HTMLDecodingInputStream extends InputStream {
 400 
 401     private final BufferedInputStream bufferedStream;
 402     private boolean descriptionParsed = false;
 403     private boolean closed = false;
 404     private int index;
 405     private int end;
 406 
 407     // InputStreamReader uses an 8K buffer. The size is not customizable.
 408     public static final int BYTE_BUFFER_LEN = 8192;
 409 
 410     // CharToByteUTF8.getMaxBytesPerChar returns 3, so we should not buffer
 411     // more chars than 3 times the number of bytes we can buffer.
 412     public static final int CHAR_BUFFER_LEN = BYTE_BUFFER_LEN / 3;
 413 
 414     private static final String FAILURE_MSG =
 415         "Unable to parse HTML description: ";
 416     private static final String INVALID_MSG = " invalid";
 417 
 418     public HTMLDecodingInputStream(InputStream bytestream) throws IOException {
 419         bufferedStream = new BufferedInputStream(bytestream, BYTE_BUFFER_LEN);
 420     }
 421 
 422     private void parseDescription() throws IOException {
 423         bufferedStream.mark(BYTE_BUFFER_LEN);
 424 
 425         BufferedReader bufferedReader = new BufferedReader
 426             (new InputStreamReader(bufferedStream, HTMLSupport.ENCODING),
 427              CHAR_BUFFER_LEN);
 428         String version = bufferedReader.readLine().trim();
 429         if (version == null || !version.startsWith(HTMLSupport.VERSION)) {
 430             // Not MS-compliant HTML text. Return raw text from read().
 431             index = 0;
 432             end = -1;
 433             bufferedStream.reset();
 434             return;
 435         }
 436 
 437         String input;
 438         boolean startHTML, endHTML, startFragment, endFragment;
 439         startHTML = endHTML = startFragment = endFragment = false;
 440 
 441         try {
 442             do {
 443                 input = bufferedReader.readLine().trim();
 444                 if (input == null) {
 445                     close();
 446                     throw new IOException(FAILURE_MSG);
 447                 } else if (input.startsWith(HTMLSupport.START_HTML)) {
 448                     int val = Integer.parseInt
 449                     (input.substring(HTMLSupport.START_HTML.length(),
 450                                      input.length()).trim());
 451                     if (val >= 0) {
 452                         index = val;
 453                         startHTML = true;
 454                     } else if (val != -1) {
 455                         close();
 456                         throw new IOException(FAILURE_MSG +
 457                                               HTMLSupport.START_HTML +
 458                                               INVALID_MSG);
 459                     }
 460                 } else if (input.startsWith(HTMLSupport.END_HTML)) {
 461                     int val = Integer.parseInt
 462                     (input.substring(HTMLSupport.END_HTML.length(),
 463                                      input.length()).trim());
 464                     if (val >= 0) {
 465                         end = val;
 466                         endHTML = true;
 467                     } else if (val != -1) {
 468                         close();
 469                         throw new IOException(FAILURE_MSG +
 470                                               HTMLSupport.END_HTML +
 471                                               INVALID_MSG);
 472                     }
 473                 } else if (!startHTML && !endHTML &&
 474                            input.startsWith(HTMLSupport.START_FRAGMENT)) {
 475                     index = Integer.parseInt
 476                     (input.substring(HTMLSupport.START_FRAGMENT.length(),
 477                                      input.length()).trim());
 478                     if (index < 0) {
 479                         close();
 480                         throw new IOException(FAILURE_MSG +
 481                                               HTMLSupport.START_FRAGMENT +
 482                                               INVALID_MSG);
 483                     }
 484                     startFragment = true;
 485                 } else if (!startHTML && !endHTML &&
 486                            input.startsWith(HTMLSupport.END_FRAGMENT)) {
 487                     end = Integer.parseInt
 488                     (input.substring(HTMLSupport.END_FRAGMENT.length(),
 489                                      input.length()).trim());
 490                     if (end < 0) {
 491                         close();
 492                         throw new IOException(FAILURE_MSG +
 493                                               HTMLSupport.END_FRAGMENT +
 494                                               INVALID_MSG);
 495                     }
 496                     endFragment = true;
 497                 }
 498             } while (!((startHTML && endHTML) ||
 499                        (startFragment && endFragment)));
 500         } catch (NumberFormatException e) {
 501             close();
 502             throw new IOException(FAILURE_MSG + e);
 503         }
 504 
 505         bufferedStream.reset();
 506 
 507         for (int i = 0; i < index; i++) {
 508             if (bufferedStream.read() == -1) {
 509                 close();
 510                 throw new IOException(FAILURE_MSG +
 511                                       "Byte stream ends in description.");
 512             }
 513         }
 514     }
 515 
 516     public int read() throws IOException {
 517         if (closed) {
 518             throw new IOException("Stream closed");
 519         }
 520 
 521         if (!descriptionParsed) {
 522             parseDescription(); // initializes 'index' and 'end'
 523             descriptionParsed = true;
 524         }
 525 
 526         if (end != -1 && index >= end) {
 527             return -1;
 528         }
 529 
 530         int retval = bufferedStream.read();
 531         if (retval == -1) {
 532             index = end = 0; // so future read() calls will fail quickly
 533             return -1;
 534         }
 535 
 536         index++;
 537     //    System.out.print((char)retval);
 538         return retval;
 539     }
 540 
 541     public void close() throws IOException {
 542         if (!closed) {
 543             closed = true;
 544             bufferedStream.close();
 545         }
 546     }
 547 }