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}s currently registered
72      * on this clipboard across all {@code AppContext}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 
SunClipboard(String name)83     public SunClipboard(String name) {
84         super(name);
85         CLIPBOARD_FLAVOR_LISTENER_KEY = new StringBuffer(name + "_CLIPBOARD_FLAVOR_LISTENER_KEY");
86     }
87 
setContents(Transferable contents, ClipboardOwner owner)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 
initContext()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 
getContents(Object requestor)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      */
getContextContents()147     protected synchronized Transferable getContextContents() {
148         AppContext context = AppContext.getAppContext();
149         return (context == contentsContext) ? contents : null;
150     }
151 
152 
153     /**
154      * @see java.awt.datatransfer.Clipboard#getAvailableDataFlavors
155      * @since 1.5
156      */
getAvailableDataFlavors()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.datatransfer.Clipboard#isDataFlavorAvailable
171      * @since 1.5
172      */
isDataFlavorAvailable(DataFlavor flavor)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.datatransfer.Clipboard#getData
190      * @since 1.5
191      */
getData(DataFlavor flavor)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      */
createLocaleTransferable(long[] formats)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      */
openClipboard(SunClipboard newOwner)245     public void openClipboard(SunClipboard newOwner) {}
closeClipboard()246     public void closeClipboard() {}
247 
getID()248     public abstract long getID();
249 
propertyChange(PropertyChangeEvent evt)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 
lostOwnershipImpl()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} and is not equal to the current
266      * contents context.
267      *
268      * @param disposedContext the AppContext that is disposed or
269      *        {@code null} if the ownership is lost because another
270      *        application acquired ownership.
271      */
lostOwnershipLater(final AppContext disposedContext)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 
lostOwnershipNow(final AppContext disposedContext)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 
clearNativeContext()313     protected abstract void clearNativeContext();
314 
setContentsNative(Transferable contents)315     protected abstract void setContentsNative(Transferable contents);
316 
317     /**
318      * @since 1.5
319      */
getClipboardFormatsOpenClose()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      */
getClipboardFormats()334     protected abstract long[] getClipboardFormats();
335 
getClipboardData(long format)336     protected abstract byte[] getClipboardData(long format) throws IOException;
337 
338 
formatArrayAsDataFlavorSet(long[] formats)339     private static Set<DataFlavor> formatArrayAsDataFlavorSet(long[] formats) {
340         return (formats == null) ? null :
341                 DataTransferer.getInstance().
342                 getFlavorsForFormatsAsSet(formats, getDefaultFlavorTable());
343     }
344 
345 
addFlavorListener(FlavorListener listener)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 
removeFlavorListener(FlavorListener listener)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")
getFlavorListeners(AppContext appContext)389     private Set<FlavorListener> getFlavorListeners(AppContext appContext) {
390         return (Set<FlavorListener>)appContext.get(CLIPBOARD_FLAVOR_LISTENER_KEY);
391     }
392 
getFlavorListeners()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 
areFlavorListenersRegistered()399     public boolean areFlavorListenersRegistered() {
400         return (numberOfFlavorListeners > 0);
401     }
402 
registerClipboardViewerChecked()403     protected abstract void registerClipboardViewerChecked();
404 
unregisterClipboardViewerChecked()405     protected abstract void unregisterClipboardViewerChecked();
406 
407     /**
408      * Checks change of the {@code DataFlavor}s and, if necessary,
409      * posts notifications on {@code FlavorEvent}s to the
410      * AppContexts' EDTs.
411      * The parameter {@code formats} 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      */
checkChange(final long[] formats)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 
getDefaultFlavorTable()444     public static FlavorTable getDefaultFlavorTable() {
445         return (FlavorTable) SystemFlavorMap.getDefaultFlavorMap();
446     }
447 }
448