1 /* 2 * Copyright (c) 1999, 2006, 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.awt.datatransfer; 27 28 import java.awt.EventQueue; 29 30 import java.awt.datatransfer.Clipboard; 31 import java.awt.datatransfer.FlavorTable; 32 import java.awt.datatransfer.SystemFlavorMap; 33 import java.awt.datatransfer.Transferable; 34 import java.awt.datatransfer.ClipboardOwner; 35 import java.awt.datatransfer.DataFlavor; 36 import java.awt.datatransfer.FlavorListener; 37 import java.awt.datatransfer.FlavorEvent; 38 import java.awt.datatransfer.UnsupportedFlavorException; 39 40 import java.beans.PropertyChangeEvent; 41 import java.beans.PropertyChangeListener; 42 43 import java.util.Objects; 44 import java.util.Set; 45 import java.util.HashSet; 46 47 import java.io.IOException; 48 49 import sun.awt.AppContext; 50 import sun.awt.PeerEvent; 51 import sun.awt.SunToolkit; 52 53 54 /** 55 * Serves as a common, helper superclass for the Win32 and X11 system 56 * Clipboards. 57 * 58 * @author Danila Sinopalnikov 59 * @author Alexander Gerasimov 60 * 61 * @since 1.3 62 */ 63 public abstract class SunClipboard extends Clipboard 64 implements PropertyChangeListener { 65 66 private AppContext contentsContext = null; 67 68 private final Object CLIPBOARD_FLAVOR_LISTENER_KEY; 69 70 /** 71 * A number of <code>FlavorListener</code>s currently registered 72 * on this clipboard across all <code>AppContext</code>s. 73 */ 74 private volatile int numberOfFlavorListeners = 0; 75 76 /** 77 * A set of <code>DataFlavor</code>s that is available on 78 * this clipboard. It is used for tracking changes 79 * of <code>DataFlavor</code>s available on this clipboard. 80 */ 81 private volatile Set<DataFlavor> currentDataFlavors; 82 83 84 public SunClipboard(String name) { 85 super(name); 86 CLIPBOARD_FLAVOR_LISTENER_KEY = new StringBuffer(name + "_CLIPBOARD_FLAVOR_LISTENER_KEY"); 87 } 88 89 public synchronized void setContents(Transferable contents, 90 ClipboardOwner owner) { 91 // 4378007 : Toolkit.getSystemClipboard().setContents(null, null) 92 // should throw NPE 93 if (contents == null) { 94 throw new NullPointerException("contents"); 95 } 96 97 initContext(); 98 99 final ClipboardOwner oldOwner = this.owner; 100 final Transferable oldContents = this.contents; 101 102 try { 103 this.owner = owner; 104 this.contents = new TransferableProxy(contents, true); 105 106 setContentsNative(contents); 107 } finally { 108 if (oldOwner != null && oldOwner != owner) { 109 EventQueue.invokeLater(() -> oldOwner.lostOwnership(SunClipboard.this, oldContents)); 110 } 111 } 112 } 113 114 private synchronized void initContext() { 115 final AppContext context = AppContext.getAppContext(); 116 117 if (contentsContext != context) { 118 // Need to synchronize on the AppContext to guarantee that it cannot 119 // be disposed after the check, but before the listener is added. 120 synchronized (context) { 121 if (context.isDisposed()) { 122 throw new IllegalStateException("Can't set contents from disposed AppContext"); 123 } 124 context.addPropertyChangeListener 125 (AppContext.DISPOSED_PROPERTY_NAME, this); 126 } 127 if (contentsContext != null) { 128 contentsContext.removePropertyChangeListener 129 (AppContext.DISPOSED_PROPERTY_NAME, this); 130 } 131 contentsContext = context; 132 } 133 } 134 135 public synchronized Transferable getContents(Object requestor) { 136 if (contents != null) { 137 return contents; 138 } 139 return new ClipboardTransferable(this); 140 } 141 142 143 /** 144 * @return the contents of this clipboard if it has been set from the same 145 * AppContext as it is currently retrieved or null otherwise 146 * @since 1.5 147 */ 148 private synchronized Transferable getContextContents() { 149 AppContext context = AppContext.getAppContext(); 150 return (context == contentsContext) ? contents : null; 151 } 152 153 154 /** 155 * @see java.awt.Clipboard#getAvailableDataFlavors 156 * @since 1.5 157 */ 158 public DataFlavor[] getAvailableDataFlavors() { 159 Transferable cntnts = getContextContents(); 160 if (cntnts != null) { 161 return cntnts.getTransferDataFlavors(); 162 } 163 164 long[] formats = getClipboardFormatsOpenClose(); 165 166 return DataTransferer.getInstance(). 167 getFlavorsForFormatsAsArray(formats, getDefaultFlavorTable()); 168 } 169 170 /** 171 * @see java.awt.Clipboard#isDataFlavorAvailable 172 * @since 1.5 173 */ 174 public boolean isDataFlavorAvailable(DataFlavor flavor) { 175 if (flavor == null) { 176 throw new NullPointerException("flavor"); 177 } 178 179 Transferable cntnts = getContextContents(); 180 if (cntnts != null) { 181 return cntnts.isDataFlavorSupported(flavor); 182 } 183 184 long[] formats = getClipboardFormatsOpenClose(); 185 186 return formatArrayAsDataFlavorSet(formats).contains(flavor); 187 } 188 189 /** 190 * @see java.awt.Clipboard#getData 191 * @since 1.5 192 */ 193 public Object getData(DataFlavor flavor) 194 throws UnsupportedFlavorException, IOException { 195 if (flavor == null) { 196 throw new NullPointerException("flavor"); 197 } 198 199 Transferable cntnts = getContextContents(); 200 if (cntnts != null) { 201 return cntnts.getTransferData(flavor); 202 } 203 204 long format = 0; 205 byte[] data = null; 206 Transferable localeTransferable = null; 207 208 try { 209 openClipboard(null); 210 211 long[] formats = getClipboardFormats(); 212 Long lFormat = DataTransferer.getInstance(). 213 getFlavorsForFormats(formats, getDefaultFlavorTable()).get(flavor); 214 215 if (lFormat == null) { 216 throw new UnsupportedFlavorException(flavor); 217 } 218 219 format = lFormat.longValue(); 220 data = getClipboardData(format); 221 222 if (DataTransferer.getInstance().isLocaleDependentTextFormat(format)) { 223 localeTransferable = createLocaleTransferable(formats); 224 } 225 226 } finally { 227 closeClipboard(); 228 } 229 230 return DataTransferer.getInstance(). 231 translateBytes(data, flavor, format, localeTransferable); 232 } 233 234 /** 235 * The clipboard must be opened. 236 * 237 * @since 1.5 238 */ 239 protected Transferable createLocaleTransferable(long[] formats) throws IOException { 240 return null; 241 } 242 243 /** 244 * @throws IllegalStateException if the clipboard has not been opened 245 */ 246 public void openClipboard(SunClipboard newOwner) {} 247 public void closeClipboard() {} 248 249 public abstract long getID(); 250 251 public void propertyChange(PropertyChangeEvent evt) { 252 if (AppContext.DISPOSED_PROPERTY_NAME.equals(evt.getPropertyName()) && 253 Boolean.TRUE.equals(evt.getNewValue())) { 254 final AppContext disposedContext = (AppContext)evt.getSource(); 255 lostOwnershipLater(disposedContext); 256 } 257 } 258 259 protected void lostOwnershipImpl() { 260 lostOwnershipLater(null); 261 } 262 263 /** 264 * Clears the clipboard state (contents, owner and contents context) and 265 * notifies the current owner that ownership is lost. Does nothing if the 266 * argument is not <code>null</code> and is not equal to the current 267 * contents context. 268 * 269 * @param disposedContext the AppContext that is disposed or 270 * <code>null</code> if the ownership is lost because another 271 * application acquired ownership. 272 */ 273 protected void lostOwnershipLater(final AppContext disposedContext) { 274 final AppContext context = this.contentsContext; 275 if (context == null) { 276 return; 277 } 278 279 final Runnable runnable = new Runnable() { 280 public void run() { 281 final SunClipboard sunClipboard = SunClipboard.this; 282 ClipboardOwner owner = null; 283 Transferable contents = null; 284 285 synchronized (sunClipboard) { 286 final AppContext context = sunClipboard.contentsContext; 287 288 if (context == null) { 289 return; 290 } 291 292 if (disposedContext == null || context == disposedContext) { 293 owner = sunClipboard.owner; 294 contents = sunClipboard.contents; 295 sunClipboard.contentsContext = null; 296 sunClipboard.owner = null; 297 sunClipboard.contents = null; 298 sunClipboard.clearNativeContext(); 299 context.removePropertyChangeListener 300 (AppContext.DISPOSED_PROPERTY_NAME, sunClipboard); 301 } else { 302 return; 303 } 304 } 305 if (owner != null) { 306 owner.lostOwnership(sunClipboard, contents); 307 } 308 } 309 }; 310 311 SunToolkit.postEvent(context, new PeerEvent(this, runnable, 312 PeerEvent.PRIORITY_EVENT)); 313 } 314 315 protected abstract void clearNativeContext(); 316 317 protected abstract void setContentsNative(Transferable contents); 318 319 /** 320 * @since 1.5 321 */ 322 protected long[] getClipboardFormatsOpenClose() { 323 try { 324 openClipboard(null); 325 return getClipboardFormats(); 326 } finally { 327 closeClipboard(); 328 } 329 } 330 331 /** 332 * Returns zero-length array (not null) if the number of available formats is zero. 333 * 334 * @throws IllegalStateException if formats could not be retrieved 335 */ 336 protected abstract long[] getClipboardFormats(); 337 338 protected abstract byte[] getClipboardData(long format) throws IOException; 339 340 341 private static Set<DataFlavor> formatArrayAsDataFlavorSet(long[] formats) { 342 return (formats == null) ? null : 343 DataTransferer.getInstance(). 344 getFlavorsForFormatsAsSet(formats, getDefaultFlavorTable()); 345 } 346 347 348 public synchronized void addFlavorListener(FlavorListener listener) { 349 if (listener == null) { 350 return; 351 } 352 AppContext appContext = AppContext.getAppContext(); 353 Set<FlavorListener> flavorListeners = getFlavorListeners(appContext); 354 if (flavorListeners == null) { 355 flavorListeners = new HashSet<>(); 356 appContext.put(CLIPBOARD_FLAVOR_LISTENER_KEY, flavorListeners); 357 } 358 flavorListeners.add(listener); 359 360 if (numberOfFlavorListeners++ == 0) { 361 long[] currentFormats = null; 362 try { 363 openClipboard(null); 364 currentFormats = getClipboardFormats(); 365 } catch (IllegalStateException exc) { 366 } finally { 367 closeClipboard(); 368 } 369 currentDataFlavors = formatArrayAsDataFlavorSet(currentFormats); 370 371 registerClipboardViewerChecked(); 372 } 373 } 374 375 public synchronized void removeFlavorListener(FlavorListener listener) { 376 if (listener == null) { 377 return; 378 } 379 Set<FlavorListener> flavorListeners = getFlavorListeners(AppContext.getAppContext()); 380 if (flavorListeners == null){ 381 //else we throw NullPointerException, but it is forbidden 382 return; 383 } 384 if (flavorListeners.remove(listener) && --numberOfFlavorListeners == 0) { 385 unregisterClipboardViewerChecked(); 386 currentDataFlavors = null; 387 } 388 } 389 390 @SuppressWarnings("unchecked") 391 private Set<FlavorListener> getFlavorListeners(AppContext appContext) { 392 return (Set<FlavorListener>)appContext.get(CLIPBOARD_FLAVOR_LISTENER_KEY); 393 } 394 395 public synchronized FlavorListener[] getFlavorListeners() { 396 Set<FlavorListener> flavorListeners = getFlavorListeners(AppContext.getAppContext()); 397 return flavorListeners == null ? new FlavorListener[0] 398 : flavorListeners.toArray(new FlavorListener[flavorListeners.size()]); 399 } 400 401 public boolean areFlavorListenersRegistered() { 402 return (numberOfFlavorListeners > 0); 403 } 404 405 protected abstract void registerClipboardViewerChecked(); 406 407 protected abstract void unregisterClipboardViewerChecked(); 408 409 /** 410 * Checks change of the <code>DataFlavor</code>s and, if necessary, 411 * posts notifications on <code>FlavorEvent</code>s to the 412 * AppContexts' EDTs. 413 * The parameter <code>formats</code> is null iff we have just 414 * failed to get formats available on the clipboard. 415 * 416 * @param formats data formats that have just been retrieved from 417 * this clipboard 418 */ 419 public void checkChange(long[] formats) { 420 Set<DataFlavor> prevDataFlavors = currentDataFlavors; 421 currentDataFlavors = formatArrayAsDataFlavorSet(formats); 422 423 if (Objects.equals(prevDataFlavors, currentDataFlavors)) { 424 // we've been able to successfully get available on the clipboard 425 // DataFlavors this and previous time and they are coincident; 426 // don't notify 427 return; 428 } 429 430 for (AppContext appContext : AppContext.getAppContexts()) { 431 if (appContext == null || appContext.isDisposed()) { 432 continue; 433 } 434 Set<FlavorListener> flavorListeners = getFlavorListeners(appContext); 435 if (flavorListeners != null) { 436 for (FlavorListener listener : flavorListeners) { 437 if (listener != null) { 438 PeerEvent peerEvent = new PeerEvent(this, 439 () -> listener.flavorsChanged(new FlavorEvent(SunClipboard.this)), 440 PeerEvent.PRIORITY_EVENT); 441 SunToolkit.postEvent(appContext, peerEvent); 442 } 443 } 444 } 445 } 446 } 447 448 public static FlavorTable getDefaultFlavorTable() { 449 return (FlavorTable) SystemFlavorMap.getDefaultFlavorMap(); 450 } 451 }