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 }