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.translateBytes(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 a byte array which contains
 265      * platform-specific image data in the given format into an Image.
 266      */
 267     protected Image platformImageBytesToImage(byte[] bytes, long format)
 268         throws IOException
 269     {
 270         return getImageForByteStream(bytes);
 271     }
 272 
 273     private native Image getImageForByteStream(byte[] bytes);
 274 
 275     @Override
 276     protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList) throws IOException {
 277         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 278         for (int i = 0; i < fileList.size(); i++)
 279         {
 280             byte[] bytes = fileList.get(i).getBytes();
 281             bos.write(bytes, 0, bytes.length);
 282             bos.write(0);
 283         }
 284         return bos;
 285     }
 286 
 287     @Override
 288     protected boolean isURIListFormat(long format) {
 289         String nat = getNativeForFormat(format);
 290         if (nat == null) {
 291             return false;
 292         }
 293         try {
 294             DataFlavor df = new DataFlavor(nat);
 295             if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) {
 296                 return true;
 297             }
 298         } catch (Exception e) {
 299             // Not a MIME format.
 300         }
 301         return false;
 302     }
 303 }
 304 
 305 
 306 // ---- Code borrowed from WDataTransferer: ----
 307 // This will come handy for supporting HTML data.
 308 
 309 final class HTMLSupport {
 310     public static final String ENCODING = "UTF-8";
 311 
 312     public static final String VERSION = "Version:";
 313     public static final String START_HTML = "StartHTML:";
 314     public static final String END_HTML = "EndHTML:";
 315     public static final String START_FRAGMENT = "StartFragment:";
 316     public static final String END_FRAGMENT = "EndFragment:";
 317     public static final String START_FRAGMENT_CMT = "<!--StartFragment-->";
 318     public static final String END_FRAGMENT_CMT = "<!--EndFragment-->";
 319     public static final String EOLN = "\r\n";
 320 
 321     private static final String VERSION_NUM = "0.9";
 322     private static final String HTML_START_END = "-1";
 323 
 324     private static final int PADDED_WIDTH = 10;
 325 
 326     private static final int HEADER_LEN =
 327         VERSION.length() + VERSION_NUM.length() + EOLN.length() +
 328         START_HTML.length() + HTML_START_END.length() + EOLN.length() +
 329         END_HTML.length() + HTML_START_END.length() + EOLN.length() +
 330         START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 331         END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() +
 332         START_FRAGMENT_CMT.length() + EOLN.length();
 333     private static final String HEADER_LEN_STR =
 334         toPaddedString(HEADER_LEN, PADDED_WIDTH);
 335 
 336     private static final String TRAILER = END_FRAGMENT_CMT + EOLN + '\0';
 337 
 338     private static String toPaddedString(int n, int width) {
 339         String string = "" + n;
 340         int len = string.length();
 341         if (n >= 0 && len < width) {
 342             char[] array = new char[width - len];
 343             Arrays.fill(array, '0');
 344             StringBuffer buffer = new StringBuffer();
 345             buffer.append(array);
 346             buffer.append(string);
 347             string = buffer.toString();
 348         }
 349         return string;
 350     }
 351 
 352     public static byte[] convertToHTMLFormat(byte[] bytes) {
 353         StringBuffer header = new StringBuffer(HEADER_LEN);
 354         header.append(VERSION);
 355         header.append(VERSION_NUM);
 356         header.append(EOLN);
 357         header.append(START_HTML);
 358         header.append(HTML_START_END);
 359         header.append(EOLN);
 360         header.append(END_HTML);
 361         header.append(HTML_START_END);
 362         header.append(EOLN);
 363         header.append(START_FRAGMENT);
 364         header.append(HEADER_LEN_STR);
 365         header.append(EOLN);
 366         header.append(END_FRAGMENT);
 367         // Strip terminating NUL byte from array
 368         header.append(toPaddedString(HEADER_LEN + bytes.length - 1,
 369                                      PADDED_WIDTH));
 370         header.append(EOLN);
 371         header.append(START_FRAGMENT_CMT);
 372         header.append(EOLN);
 373 
 374         byte[] headerBytes = null, trailerBytes = null;
 375 
 376         try {
 377             headerBytes = new String(header).getBytes(ENCODING);
 378             trailerBytes = TRAILER.getBytes(ENCODING);
 379         } catch (UnsupportedEncodingException cannotHappen) {
 380         }
 381 
 382         byte[] retval = new byte[headerBytes.length + bytes.length - 1 +
 383             trailerBytes.length];
 384 
 385         System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length);
 386         System.arraycopy(bytes, 0, retval, headerBytes.length,
 387                          bytes.length - 1);
 388         System.arraycopy(trailerBytes, 0, retval,
 389                          headerBytes.length + bytes.length - 1,
 390                          trailerBytes.length);
 391 
 392         return retval;
 393     }
 394 }
 395 
 396 /**
 397 * This stream takes an InputStream which provides data in CF_HTML format,
 398  * strips off the description and context to extract the original HTML data.
 399  */
 400 class HTMLDecodingInputStream extends InputStream {
 401 
 402     private final BufferedInputStream bufferedStream;
 403     private boolean descriptionParsed = false;
 404     private boolean closed = false;
 405     private int index;
 406     private int end;
 407 
 408     // InputStreamReader uses an 8K buffer. The size is not customizable.
 409     public static final int BYTE_BUFFER_LEN = 8192;
 410 
 411     // CharToByteUTF8.getMaxBytesPerChar returns 3, so we should not buffer
 412     // more chars than 3 times the number of bytes we can buffer.
 413     public static final int CHAR_BUFFER_LEN = BYTE_BUFFER_LEN / 3;
 414 
 415     private static final String FAILURE_MSG =
 416         "Unable to parse HTML description: ";
 417     private static final String INVALID_MSG = " invalid";
 418 
 419     public HTMLDecodingInputStream(InputStream bytestream) throws IOException {
 420         bufferedStream = new BufferedInputStream(bytestream, BYTE_BUFFER_LEN);
 421     }
 422 
 423     private void parseDescription() throws IOException {
 424         bufferedStream.mark(BYTE_BUFFER_LEN);
 425 
 426         BufferedReader bufferedReader = new BufferedReader
 427             (new InputStreamReader(bufferedStream, HTMLSupport.ENCODING),
 428              CHAR_BUFFER_LEN);
 429         String version = bufferedReader.readLine().trim();
 430         if (version == null || !version.startsWith(HTMLSupport.VERSION)) {
 431             // Not MS-compliant HTML text. Return raw text from read().
 432             index = 0;
 433             end = -1;
 434             bufferedStream.reset();
 435             return;
 436         }
 437 
 438         String input;
 439         boolean startHTML, endHTML, startFragment, endFragment;
 440         startHTML = endHTML = startFragment = endFragment = false;
 441 
 442         try {
 443             do {
 444                 input = bufferedReader.readLine().trim();
 445                 if (input == null) {
 446                     close();
 447                     throw new IOException(FAILURE_MSG);
 448                 } else if (input.startsWith(HTMLSupport.START_HTML)) {
 449                     int val = Integer.parseInt
 450                     (input.substring(HTMLSupport.START_HTML.length(),
 451                                      input.length()).trim());
 452                     if (val >= 0) {
 453                         index = val;
 454                         startHTML = true;
 455                     } else if (val != -1) {
 456                         close();
 457                         throw new IOException(FAILURE_MSG +
 458                                               HTMLSupport.START_HTML +
 459                                               INVALID_MSG);
 460                     }
 461                 } else if (input.startsWith(HTMLSupport.END_HTML)) {
 462                     int val = Integer.parseInt
 463                     (input.substring(HTMLSupport.END_HTML.length(),
 464                                      input.length()).trim());
 465                     if (val >= 0) {
 466                         end = val;
 467                         endHTML = true;
 468                     } else if (val != -1) {
 469                         close();
 470                         throw new IOException(FAILURE_MSG +
 471                                               HTMLSupport.END_HTML +
 472                                               INVALID_MSG);
 473                     }
 474                 } else if (!startHTML && !endHTML &&
 475                            input.startsWith(HTMLSupport.START_FRAGMENT)) {
 476                     index = Integer.parseInt
 477                     (input.substring(HTMLSupport.START_FRAGMENT.length(),
 478                                      input.length()).trim());
 479                     if (index < 0) {
 480                         close();
 481                         throw new IOException(FAILURE_MSG +
 482                                               HTMLSupport.START_FRAGMENT +
 483                                               INVALID_MSG);
 484                     }
 485                     startFragment = true;
 486                 } else if (!startHTML && !endHTML &&
 487                            input.startsWith(HTMLSupport.END_FRAGMENT)) {
 488                     end = Integer.parseInt
 489                     (input.substring(HTMLSupport.END_FRAGMENT.length(),
 490                                      input.length()).trim());
 491                     if (end < 0) {
 492                         close();
 493                         throw new IOException(FAILURE_MSG +
 494                                               HTMLSupport.END_FRAGMENT +
 495                                               INVALID_MSG);
 496                     }
 497                     endFragment = true;
 498                 }
 499             } while (!((startHTML && endHTML) ||
 500                        (startFragment && endFragment)));
 501         } catch (NumberFormatException e) {
 502             close();
 503             throw new IOException(FAILURE_MSG + e);
 504         }
 505 
 506         bufferedStream.reset();
 507 
 508         for (int i = 0; i < index; i++) {
 509             if (bufferedStream.read() == -1) {
 510                 close();
 511                 throw new IOException(FAILURE_MSG +
 512                                       "Byte stream ends in description.");
 513             }
 514         }
 515     }
 516 
 517     public int read() throws IOException {
 518         if (closed) {
 519             throw new IOException("Stream closed");
 520         }
 521 
 522         if (!descriptionParsed) {
 523             parseDescription(); // initializes 'index' and 'end'
 524             descriptionParsed = true;
 525         }
 526 
 527         if (end != -1 && index >= end) {
 528             return -1;
 529         }
 530 
 531         int retval = bufferedStream.read();
 532         if (retval == -1) {
 533             index = end = 0; // so future read() calls will fail quickly
 534             return -1;
 535         }
 536 
 537         index++;
 538     //    System.out.print((char)retval);
 539         return retval;
 540     }
 541 
 542     public void close() throws IOException {
 543         if (!closed) {
 544             closed = true;
 545             bufferedStream.close();
 546         }
 547     }
 548 }