1 /* 2 * Copyright (c) 2010, 2015, 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 com.sun.javafx.tk.quantum; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.FilePermission; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.ObjectInput; 35 import java.io.ObjectInputStream; 36 import java.io.ObjectOutput; 37 import java.io.ObjectOutputStream; 38 import java.io.Serializable; 39 import java.net.MalformedURLException; 40 import java.net.SocketPermission; 41 import java.net.URL; 42 import java.nio.ByteBuffer; 43 import java.security.AccessControlContext; 44 import java.security.Permission; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.EnumSet; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Set; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 54 import javafx.scene.image.Image; 55 import javafx.scene.input.DataFormat; 56 import javafx.scene.input.TransferMode; 57 import javafx.util.Pair; 58 59 import com.sun.glass.ui.Application; 60 import com.sun.glass.ui.Clipboard; 61 import com.sun.glass.ui.ClipboardAssistance; 62 import com.sun.glass.ui.Pixels; 63 import com.sun.javafx.tk.ImageLoader; 64 import com.sun.javafx.tk.PermissionHelper; 65 import com.sun.javafx.tk.TKClipboard; 66 import com.sun.javafx.tk.Toolkit; 67 import javafx.scene.image.PixelReader; 68 import java.io.ObjectStreamClass; 69 import javafx.scene.image.WritablePixelFormat; 70 71 /** 72 * The implementation of TKClipboard, which is used both for clipboards 73 * and dragboards. 74 */ 75 final class QuantumClipboard implements TKClipboard { 76 77 /** 78 * Handle to the Glass peer. 79 */ 80 private ClipboardAssistance systemAssistant; 81 82 /** 83 * Security access context for image loading 84 * com.sun.javafx.tk.quantum.QuantumClipboard 85 * javafx.scene.input.Clipboard 86 * ... user code ... 87 */ 88 private AccessControlContext accessContext = null; 89 90 /** 91 * Distinguishes between clipboard and dragboard. This is needed 92 * because dragboard's flush() starts DnD operation so it mustn't be 93 * called too early. 94 */ 95 private boolean isCaching; 96 97 /** 98 * Cache of the data used for dragboard between setting them and flushing 99 * them to system dragboard. 100 */ 101 private List<Pair<DataFormat, Object>> dataCache; 102 103 /** 104 * Cache of the transfer modes used for dragboard between setting them and 105 * flushing them to system dragboard. 106 */ 107 private Set<TransferMode> transferModesCache; 108 109 /** 110 * An image which is displayed during the drag operation. Set by the user. 111 */ 112 private Image dragImage = null; 113 114 /** 115 * An offset of the image which is displayed during the drag operation. 116 * Set by the user. 117 */ 118 private double dragOffsetX = 0; 119 private double dragOffsetY = 0; 120 121 private static ClipboardAssistance currentDragboard; 122 123 /** 124 * Disallow direct creation of QuantumClipboard 125 */ 126 private QuantumClipboard() { 127 } 128 129 @Override public void setSecurityContext(AccessControlContext acc) { 130 if (accessContext != null) { 131 throw new RuntimeException("Clipboard security context has been already set!"); 132 } 133 accessContext = acc; 134 } 135 136 private AccessControlContext getAccessControlContext() { 137 if (accessContext == null) { 138 throw new RuntimeException("Clipboard security context has not been set!"); 139 } 140 return accessContext; 141 } 142 143 /** 144 * Gets an instance of QuantumClipboard for the given assistant. This may be 145 * a new instance after each call. 146 * @param assistant 147 * @return 148 */ 149 public static QuantumClipboard getClipboardInstance(ClipboardAssistance assistant) { 150 QuantumClipboard c = new QuantumClipboard(); 151 c.systemAssistant = assistant; 152 c.isCaching = false; 153 return c; 154 } 155 156 static ClipboardAssistance getCurrentDragboard() { 157 return currentDragboard; 158 } 159 160 static void releaseCurrentDragboard() { 161 // RT-34510: assert currentDragboard != null; 162 currentDragboard = null; 163 } 164 165 /** 166 * Gets an instance of QuantumClipboard for the given assistant for usage 167 * as dragboard during drag and drop. It doesn't flush the data implicitly 168 * and caches them until flush() is called. This may be 169 * a new instance after each call. 170 * @param assistant 171 * @return 172 */ 173 public static QuantumClipboard getDragboardInstance(ClipboardAssistance assistant, boolean isDragSource) { 174 QuantumClipboard c = new QuantumClipboard(); 175 c.systemAssistant = assistant; 176 c.isCaching = true; 177 if (isDragSource) { 178 currentDragboard = assistant; 179 } 180 return c; 181 } 182 183 public static int transferModesToClipboardActions(final Set<TransferMode> tms) { 184 int actions = Clipboard.ACTION_NONE; 185 for (TransferMode t : tms) { 186 switch (t) { 187 case COPY: 188 actions |= Clipboard.ACTION_COPY; 189 break; 190 case MOVE: 191 actions |= Clipboard.ACTION_MOVE; 192 break; 193 case LINK: 194 actions |= Clipboard.ACTION_REFERENCE; 195 break; 196 default: 197 throw new IllegalArgumentException( 198 "unsupported TransferMode " + tms); 199 } 200 } 201 return actions; 202 } 203 204 public void setSupportedTransferMode(Set<TransferMode> tm) { 205 if (isCaching) { 206 transferModesCache = tm; 207 } 208 final int actions = transferModesToClipboardActions(tm); 209 systemAssistant.setSupportedActions(actions); 210 } 211 212 public static Set<TransferMode> clipboardActionsToTransferModes(final int actions) { 213 final Set<TransferMode> tms = EnumSet.noneOf(TransferMode.class); 214 215 if ((actions & Clipboard.ACTION_COPY) != 0) { 216 tms.add(TransferMode.COPY); 217 } 218 if ((actions & Clipboard.ACTION_MOVE) != 0) { 219 tms.add(TransferMode.MOVE); 220 } 221 if ((actions & Clipboard.ACTION_REFERENCE) != 0) { 222 tms.add(TransferMode.LINK); 223 } 224 225 return tms; 226 } 227 228 @Override public Set<TransferMode> getTransferModes() { 229 if (transferModesCache != null) { 230 return EnumSet.copyOf(transferModesCache); 231 } 232 233 ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; 234 final Set<TransferMode> tms = clipboardActionsToTransferModes(assistant.getSupportedSourceActions()); 235 236 return tms; 237 } 238 239 @Override public void setDragView(Image image) { 240 dragImage = image; 241 } 242 243 @Override public void setDragViewOffsetX(double offsetX) { 244 dragOffsetX = offsetX; 245 } 246 247 @Override public void setDragViewOffsetY(double offsetY) { 248 dragOffsetY = offsetY; 249 } 250 251 @Override public Image getDragView() { 252 return dragImage; 253 } 254 255 @Override public double getDragViewOffsetX() { 256 return dragOffsetX; 257 } 258 259 @Override public double getDragViewOffsetY() { 260 return dragOffsetY; 261 } 262 263 public void close() { 264 systemAssistant.close(); 265 } 266 267 public void flush() { 268 if (isCaching) { 269 putContentToPeer(dataCache.toArray(new Pair[0])); 270 } 271 272 clearCache(); 273 clearDragView(); 274 systemAssistant.flush(); 275 } 276 277 @Override public Object getContent(DataFormat dataFormat) { 278 if (dataCache != null) { 279 for (Pair<DataFormat, Object> pair : dataCache) { 280 if (pair.getKey() == dataFormat) { 281 return pair.getValue(); 282 } 283 } 284 return null; 285 } 286 287 ClipboardAssistance assistant = 288 (currentDragboard != null) ? currentDragboard : systemAssistant; 289 290 if (dataFormat == DataFormat.IMAGE) { 291 return readImage(); 292 } else if (dataFormat == DataFormat.URL) { 293 return assistant.getData(Clipboard.URI_TYPE); 294 } else if (dataFormat == DataFormat.FILES) { 295 Object data = assistant.getData(Clipboard.FILE_LIST_TYPE); 296 if (data == null) return Collections.emptyList(); 297 String[] paths = (String[]) data; 298 List<File> list = new ArrayList<File>(paths.length); 299 for (int i=0; i<paths.length; i++) { 300 list.add(new File(paths[i])); 301 } 302 return list; 303 } 304 305 for (String mimeType : dataFormat.getIdentifiers()) { 306 Object data = assistant.getData(mimeType); 307 if (data instanceof ByteBuffer) { 308 try { 309 ByteBuffer bb = (ByteBuffer) data; 310 ByteArrayInputStream bis = new ByteArrayInputStream( 311 bb.array()); 312 ObjectInput in = new ObjectInputStream(bis) { 313 @Override protected Class<?> resolveClass( 314 ObjectStreamClass desc) 315 throws IOException, ClassNotFoundException { 316 return Class.forName(desc.getName(), false, 317 Thread.currentThread().getContextClassLoader()); 318 } 319 }; 320 data = in.readObject(); 321 } catch (IOException e) { 322 // ignore, just return the ByteBuffer if we cannot parse it 323 } catch (ClassNotFoundException e) { 324 // ignore, just return the ByteBuffer if we cannot parse it 325 } 326 } 327 if (data != null) return data; 328 } 329 return null; 330 } 331 332 private static Image convertObjectToImage(Object obj) { 333 if (obj instanceof Image) { 334 return (Image) obj; 335 } else { 336 final Pixels pixels; 337 if (obj instanceof ByteBuffer) { 338 ByteBuffer bb = (ByteBuffer)obj; 339 try { 340 bb.rewind(); 341 int width = bb.getInt(); 342 int height = bb.getInt(); 343 pixels = Application.GetApplication().createPixels( 344 width, height, bb.slice()); 345 } catch (Exception e) { 346 //ignore incorrect sized arrays 347 //not a client problem 348 return null; 349 } 350 } else if (obj instanceof Pixels) { 351 pixels = (Pixels)obj; 352 } else { 353 return null; 354 } 355 com.sun.prism.Image platformImage = PixelUtils.pixelsToImage( 356 pixels); 357 ImageLoader il = Toolkit.getToolkit().loadPlatformImage( 358 platformImage); 359 return Image.impl_fromPlatformImage(il); 360 } 361 } 362 363 private Image readImage() { 364 ClipboardAssistance assistant = 365 (currentDragboard != null) ? currentDragboard : systemAssistant; 366 367 Object rawData = assistant.getData(Clipboard.RAW_IMAGE_TYPE); 368 if (rawData == null) { 369 Object htmlData = assistant.getData(Clipboard.HTML_TYPE); 370 if (htmlData != null) { 371 String url = parseIMG(htmlData); 372 if (url != null) { 373 try { 374 SecurityManager sm = System.getSecurityManager(); 375 if (sm != null) { 376 AccessControlContext context = getAccessControlContext(); 377 URL u = new URL(url); 378 String protocol = u.getProtocol(); 379 if (protocol.equalsIgnoreCase("jar")) { 380 String file = u.getFile(); 381 u = new URL(file); 382 protocol = u.getProtocol(); 383 } 384 if (protocol.equalsIgnoreCase("file")) { 385 FilePermission fp = new FilePermission(u.getFile(), "read"); 386 context.checkPermission(fp); 387 } else if (protocol.equalsIgnoreCase("ftp") || 388 protocol.equalsIgnoreCase("http") || 389 protocol.equalsIgnoreCase("https")) { 390 int port = u.getPort(); 391 String hoststr = (port == -1 ? u.getHost() : u.getHost() + ":" + port); 392 SocketPermission sp = new SocketPermission(hoststr, "connect"); 393 context.checkPermission(sp); 394 } else { 395 final Permission clipboardPerm = 396 PermissionHelper.getAccessClipboardPermission(); 397 context.checkPermission(clipboardPerm); 398 } 399 } 400 return (new Image(url)); 401 } catch (MalformedURLException mue) { 402 return null; 403 } catch (SecurityException se) { 404 return null; 405 } 406 } 407 } 408 return null; 409 } 410 return convertObjectToImage(rawData); 411 } 412 413 private static final Pattern findTagIMG = 414 Pattern.compile("IMG\\s+SRC=\\\"([^\\\"]+)\\\"", 415 Pattern.DOTALL | Pattern.CASE_INSENSITIVE); 416 417 private String parseIMG(Object data) { 418 if (data == null) { 419 return null; 420 } 421 if ((data instanceof String) == false) { 422 return null; 423 } 424 String str = (String)data; 425 Matcher matcher = findTagIMG.matcher(str); 426 if (matcher.find()) { 427 return (matcher.group(1)); 428 } else { 429 return null; 430 } 431 } 432 433 private boolean placeImage(final Image image) { 434 if (image == null) { 435 return false; 436 } 437 438 String url = image.impl_getUrl(); 439 if (url == null || PixelUtils.supportedFormatType(url)) { 440 com.sun.prism.Image prismImage = 441 (com.sun.prism.Image)image.impl_getPlatformImage(); 442 Pixels pixels = PixelUtils.imageToPixels(prismImage); 443 if (pixels != null) { 444 systemAssistant.setData(Clipboard.RAW_IMAGE_TYPE, pixels); 445 return true; 446 } else { 447 return false; 448 } 449 } else { 450 systemAssistant.setData(Clipboard.URI_TYPE, url); 451 return true; 452 } 453 } 454 455 @Override public Set<DataFormat> getContentTypes() { 456 Set<DataFormat> set = new HashSet<DataFormat>(); 457 458 if (dataCache != null) { 459 for (Pair<DataFormat, Object> pair : dataCache) { 460 set.add(pair.getKey()); 461 } 462 return set; 463 } 464 465 ClipboardAssistance assistant = 466 (currentDragboard != null) ? currentDragboard : systemAssistant; 467 468 String[] types = assistant.getMimeTypes(); 469 if (types == null) { 470 return set; 471 } 472 for (String t: types) { 473 if (t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) { 474 set.add(DataFormat.IMAGE); 475 } else if (t.equalsIgnoreCase(Clipboard.URI_TYPE)) { 476 set.add(DataFormat.URL); 477 } else if (t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) { 478 set.add(DataFormat.FILES); 479 } else if (t.equalsIgnoreCase(Clipboard.HTML_TYPE)) { 480 set.add(DataFormat.HTML); 481 // RT-16812 - IE puts images on the clipboard in a HTML IMG url 482 try { 483 //HTML header could be improperly formatted and we can get an exception here 484 if (parseIMG(assistant.getData(Clipboard.HTML_TYPE)) != null) { 485 set.add(DataFormat.IMAGE); 486 } 487 } catch (Exception ex) { 488 //do nothing - it was just an attempt 489 } 490 } else { 491 DataFormat dataFormat = DataFormat.lookupMimeType(t); 492 if (dataFormat == null) { 493 //The user is interested in any format. 494 dataFormat = new DataFormat(t); 495 } 496 set.add(dataFormat); 497 } 498 } 499 return set; 500 } 501 502 @Override public boolean hasContent(DataFormat dataFormat) { 503 if (dataCache != null) { 504 for (Pair<DataFormat, Object> pair : dataCache) { 505 if (pair.getKey() == dataFormat) { 506 return true; 507 } 508 } 509 return false; 510 } 511 512 ClipboardAssistance assistant = 513 (currentDragboard != null) ? currentDragboard : systemAssistant; 514 515 String[] stypes = assistant.getMimeTypes(); 516 if (stypes == null) { 517 return false; 518 } 519 for (String t: stypes) { 520 if (dataFormat == DataFormat.IMAGE && 521 t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) { 522 return true; 523 } else if (dataFormat == DataFormat.URL && 524 t.equalsIgnoreCase(Clipboard.URI_TYPE)) { 525 return true; 526 } else if (dataFormat == DataFormat.IMAGE && 527 t.equalsIgnoreCase(Clipboard.HTML_TYPE) && 528 parseIMG(assistant.getData(Clipboard.HTML_TYPE)) != null) { 529 return true; 530 } else if (dataFormat == DataFormat.FILES && 531 t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) { 532 return true; 533 } 534 535 DataFormat found = DataFormat.lookupMimeType(t); 536 if (found != null && found.equals(dataFormat)) { 537 return true; 538 } 539 } 540 return false; 541 } 542 543 private static ByteBuffer prepareImage(Image image) { 544 PixelReader pr = image.getPixelReader(); 545 546 int w = (int) image.getWidth(); 547 int h = (int) image.getHeight(); 548 549 byte[] pixels = new byte[w * h * 4]; 550 pr.getPixels(0, 0, w, h, WritablePixelFormat.getByteBgraInstance(), pixels, 0, w*4); 551 552 ByteBuffer dragImageBuffer = ByteBuffer.allocate(8 + w * h * 4); 553 dragImageBuffer.putInt(w); 554 dragImageBuffer.putInt(h); 555 dragImageBuffer.put(pixels); 556 557 return dragImageBuffer; 558 } 559 560 private static ByteBuffer prepareOffset(double offsetX, double offsetY) { 561 ByteBuffer dragImageOffset = ByteBuffer.allocate(8); 562 563 dragImageOffset.rewind(); 564 dragImageOffset.putInt((int) offsetX); 565 dragImageOffset.putInt((int) offsetY); 566 567 return dragImageOffset; 568 } 569 570 private boolean putContentToPeer(Pair<DataFormat, Object>... content) { 571 systemAssistant.emptyCache(); 572 573 boolean dataSet = false; 574 575 // For each pair, we need to extract the DataFormat and data associated with 576 // that pair. We then will send the data down to Glass, having done 577 // some work for well known types (such as Image and File) in order to 578 // adapt from FX to Glass requirements. For everything else, we just 579 // pass down the mime type and data, for each mime type supported by 580 // the DataFormat type. 581 for (Pair<DataFormat, Object> pair : content) { 582 final DataFormat dataFormat = pair.getKey(); 583 Object data = pair.getValue(); 584 585 // Images are handled specially. On Windows, the image type supported 586 // on the clipboard is a DIB (device independent bitmap), while on Mac 587 // it is a TIFF. Other native apps expect this entry on the clipboard 588 // for image copy/paste. However other Java apps or FX apps (for example) 589 // might expect the JPG bits directly, rather than the DIB / TIFF bits. 590 // So what we do is, any IMAGE type DataFormat that comes in will be stored 591 // in DIB / TIFF, while specific bits will also be stored (in the future). 592 if (dataFormat == DataFormat.IMAGE) { 593 dataSet = placeImage(convertObjectToImage(data)); 594 } else if (dataFormat == DataFormat.URL) { 595 // TODO Weird, but this is how Glass wants it... 596 systemAssistant.setData(Clipboard.URI_TYPE, data); 597 dataSet = true; 598 } else if (dataFormat == DataFormat.RTF) { 599 systemAssistant.setData(Clipboard.RTF_TYPE, data); 600 dataSet = true; 601 } else if (dataFormat == DataFormat.FILES) { 602 // Have to convert from List<File> to String[] 603 List<File> list = (List<File>)data; 604 if (list.size() != 0) { 605 String[] paths = new String[list.size()]; 606 int i = 0; 607 for (File f : list) { 608 paths[i++] = f.getAbsolutePath(); 609 } 610 systemAssistant.setData(Clipboard.FILE_LIST_TYPE, paths); 611 dataSet = true; 612 } 613 } else { 614 if (data instanceof Serializable) { 615 if ((dataFormat != DataFormat.PLAIN_TEXT && dataFormat != DataFormat.HTML) || 616 !(data instanceof String)) 617 { 618 try { 619 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 620 ObjectOutput out = new ObjectOutputStream(bos); 621 out.writeObject(data); 622 out.close(); 623 data = ByteBuffer.wrap(bos.toByteArray()); 624 } catch (IOException e) { 625 throw new IllegalArgumentException("Could not serialize the data", e); 626 } 627 } 628 } else if (data instanceof InputStream) { 629 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 630 try (InputStream is = (InputStream)data) { 631 // TODO: performance 632 int i = is.read(); 633 while (i != -1) { 634 bout.write(i); 635 i = is.read(); 636 } 637 } catch (IOException e) { 638 throw new IllegalArgumentException("Could not serialize the data", e); 639 } 640 data = ByteBuffer.wrap(bout.toByteArray()); 641 } else if (!(data instanceof ByteBuffer)) { 642 throw new IllegalArgumentException("Only serializable " 643 + "objects or ByteBuffer can be used as data " 644 + "with data format " + dataFormat); 645 } 646 for (String mimeType : dataFormat.getIdentifiers()) { 647 systemAssistant.setData(mimeType, data); 648 dataSet = true; 649 } 650 } 651 } 652 653 // add drag image and offsets to the peer 654 if (dragImage != null) { 655 ByteBuffer imageBuffer = prepareImage(dragImage); 656 ByteBuffer offsetBuffer = prepareOffset(dragOffsetX, dragOffsetY); 657 systemAssistant.setData(Clipboard.DRAG_IMAGE, imageBuffer); 658 systemAssistant.setData(Clipboard.DRAG_IMAGE_OFFSET, offsetBuffer); 659 } 660 661 return dataSet; 662 } 663 664 665 @Override public boolean putContent(Pair<DataFormat, Object>... content) { 666 667 for (Pair<DataFormat, Object> pair : content) { 668 final DataFormat format = pair.getKey(); 669 final Object data = pair.getValue(); 670 671 if (format == null) { 672 throw new NullPointerException("Clipboard.putContent: null data format"); 673 } 674 if (data == null) { 675 throw new NullPointerException("Clipboard.putContent: null data"); 676 } 677 } 678 679 boolean dataSet = false; 680 681 if (isCaching) { 682 if (dataCache == null) { 683 dataCache = new ArrayList<Pair<DataFormat, Object>>(content.length); 684 } 685 for (Pair<DataFormat, Object> pair : content) { 686 dataCache.add(pair); 687 dataSet = true; 688 } 689 } else { 690 dataSet = putContentToPeer(content); 691 systemAssistant.flush(); 692 } 693 694 return dataSet; 695 } 696 697 private void clearCache() { 698 dataCache = null; 699 transferModesCache = null; 700 } 701 702 private void clearDragView() { 703 dragImage = null; 704 dragOffsetX = dragOffsetY = 0; 705 } 706 }