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