/*
 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.javafx.tk.quantum;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.SocketPermission;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.AccessControlContext;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.scene.image.Image;
import javafx.scene.input.DataFormat;
import javafx.scene.input.TransferMode;
import javafx.util.Pair;

import com.sun.glass.ui.Application;
import com.sun.glass.ui.Clipboard;
import com.sun.glass.ui.ClipboardAssistance;
import com.sun.glass.ui.Pixels;
import com.sun.javafx.tk.ImageLoader;
import com.sun.javafx.tk.PermissionHelper;
import com.sun.javafx.tk.TKClipboard;
import com.sun.javafx.tk.Toolkit;
import javafx.scene.image.PixelReader;
import java.io.ObjectStreamClass;
import javafx.scene.image.WritablePixelFormat;

The implementation of TKClipboard, which is used both for clipboards and dragboards.
/** * The implementation of TKClipboard, which is used both for clipboards * and dragboards. */
final class QuantumClipboard implements TKClipboard {
Handle to the Glass peer.
/** * Handle to the Glass peer. */
private ClipboardAssistance systemAssistant;
Security access context for image loading com.sun.javafx.tk.quantum.QuantumClipboard javafx.scene.input.Clipboard ... user code ...
/** * Security access context for image loading * com.sun.javafx.tk.quantum.QuantumClipboard * javafx.scene.input.Clipboard * ... user code ... */
private AccessControlContext accessContext = null;
Distinguishes between clipboard and dragboard. This is needed because dragboard's flush() starts DnD operation so it mustn't be called too early.
/** * Distinguishes between clipboard and dragboard. This is needed * because dragboard's flush() starts DnD operation so it mustn't be * called too early. */
private boolean isCaching;
Cache of the data used for dragboard between setting them and flushing them to system dragboard.
/** * Cache of the data used for dragboard between setting them and flushing * them to system dragboard. */
private List<Pair<DataFormat, Object>> dataCache;
Cache of the transfer modes used for dragboard between setting them and flushing them to system dragboard.
/** * Cache of the transfer modes used for dragboard between setting them and * flushing them to system dragboard. */
private Set<TransferMode> transferModesCache;
An image which is displayed during the drag operation. Set by the user.
/** * An image which is displayed during the drag operation. Set by the user. */
private Image dragImage = null;
An offset of the image which is displayed during the drag operation. Set by the user.
/** * An offset of the image which is displayed during the drag operation. * Set by the user. */
private double dragOffsetX = 0; private double dragOffsetY = 0; private static ClipboardAssistance currentDragboard;
Disallow direct creation of QuantumClipboard
/** * Disallow direct creation of QuantumClipboard */
private QuantumClipboard() { } @Override public void setSecurityContext(AccessControlContext acc) { if (accessContext != null) { throw new RuntimeException("Clipboard security context has been already set!"); } accessContext = acc; } private AccessControlContext getAccessControlContext() { if (accessContext == null) { throw new RuntimeException("Clipboard security context has not been set!"); } return accessContext; }
Gets an instance of QuantumClipboard for the given assistant. This may be a new instance after each call.
Params:
  • assistant –
Returns:
/** * Gets an instance of QuantumClipboard for the given assistant. This may be * a new instance after each call. * @param assistant * @return */
public static QuantumClipboard getClipboardInstance(ClipboardAssistance assistant) { QuantumClipboard c = new QuantumClipboard(); c.systemAssistant = assistant; c.isCaching = false; return c; } static ClipboardAssistance getCurrentDragboard() { return currentDragboard; } static void releaseCurrentDragboard() { // RT-34510: assert currentDragboard != null; currentDragboard = null; }
Gets an instance of QuantumClipboard for the given assistant for usage as dragboard during drag and drop. It doesn't flush the data implicitly and caches them until flush() is called. This may be a new instance after each call.
Params:
  • assistant –
Returns:
/** * Gets an instance of QuantumClipboard for the given assistant for usage * as dragboard during drag and drop. It doesn't flush the data implicitly * and caches them until flush() is called. This may be * a new instance after each call. * @param assistant * @return */
public static QuantumClipboard getDragboardInstance(ClipboardAssistance assistant, boolean isDragSource) { QuantumClipboard c = new QuantumClipboard(); c.systemAssistant = assistant; c.isCaching = true; if (isDragSource) { currentDragboard = assistant; } return c; } public static int transferModesToClipboardActions(final Set<TransferMode> tms) { int actions = Clipboard.ACTION_NONE; for (TransferMode t : tms) { switch (t) { case COPY: actions |= Clipboard.ACTION_COPY; break; case MOVE: actions |= Clipboard.ACTION_MOVE; break; case LINK: actions |= Clipboard.ACTION_REFERENCE; break; default: throw new IllegalArgumentException( "unsupported TransferMode " + tms); } } return actions; } public void setSupportedTransferMode(Set<TransferMode> tm) { if (isCaching) { transferModesCache = tm; } final int actions = transferModesToClipboardActions(tm); systemAssistant.setSupportedActions(actions); } public static Set<TransferMode> clipboardActionsToTransferModes(final int actions) { final Set<TransferMode> tms = EnumSet.noneOf(TransferMode.class); if ((actions & Clipboard.ACTION_COPY) != 0) { tms.add(TransferMode.COPY); } if ((actions & Clipboard.ACTION_MOVE) != 0) { tms.add(TransferMode.MOVE); } if ((actions & Clipboard.ACTION_REFERENCE) != 0) { tms.add(TransferMode.LINK); } return tms; } @Override public Set<TransferMode> getTransferModes() { if (transferModesCache != null) { return EnumSet.copyOf(transferModesCache); } ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; final Set<TransferMode> tms = clipboardActionsToTransferModes(assistant.getSupportedSourceActions()); return tms; } @Override public void setDragView(Image image) { dragImage = image; } @Override public void setDragViewOffsetX(double offsetX) { dragOffsetX = offsetX; } @Override public void setDragViewOffsetY(double offsetY) { dragOffsetY = offsetY; } @Override public Image getDragView() { return dragImage; } @Override public double getDragViewOffsetX() { return dragOffsetX; } @Override public double getDragViewOffsetY() { return dragOffsetY; } public void close() { systemAssistant.close(); } public void flush() { if (isCaching) { putContentToPeer(dataCache.toArray(new Pair[0])); } clearCache(); clearDragView(); systemAssistant.flush(); } @Override public Object getContent(DataFormat dataFormat) { if (dataCache != null) { for (Pair<DataFormat, Object> pair : dataCache) { if (pair.getKey() == dataFormat) { return pair.getValue(); } } return null; } ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; if (dataFormat == DataFormat.IMAGE) { return readImage(); } else if (dataFormat == DataFormat.URL) { return assistant.getData(Clipboard.URI_TYPE); } else if (dataFormat == DataFormat.FILES) { Object data = assistant.getData(Clipboard.FILE_LIST_TYPE); if (data == null) return Collections.emptyList(); String[] paths = (String[]) data; List<File> list = new ArrayList<File>(paths.length); for (int i=0; i<paths.length; i++) { list.add(new File(paths[i])); } return list; } for (String mimeType : dataFormat.getIdentifiers()) { Object data = assistant.getData(mimeType); if (data instanceof ByteBuffer) { try { ByteBuffer bb = (ByteBuffer) data; ByteArrayInputStream bis = new ByteArrayInputStream( bb.array()); ObjectInput in = new ObjectInputStream(bis) { @Override protected Class<?> resolveClass( ObjectStreamClass desc) throws IOException, ClassNotFoundException { return Class.forName(desc.getName(), false, Thread.currentThread().getContextClassLoader()); } }; data = in.readObject(); } catch (IOException e) { // ignore, just return the ByteBuffer if we cannot parse it } catch (ClassNotFoundException e) { // ignore, just return the ByteBuffer if we cannot parse it } } if (data != null) return data; } return null; } private static Image convertObjectToImage(Object obj) { if (obj instanceof Image) { return (Image) obj; } else { final Pixels pixels; if (obj instanceof ByteBuffer) { ByteBuffer bb = (ByteBuffer)obj; try { bb.rewind(); int width = bb.getInt(); int height = bb.getInt(); pixels = Application.GetApplication().createPixels( width, height, bb.slice()); } catch (Exception e) { //ignore incorrect sized arrays //not a client problem return null; } } else if (obj instanceof Pixels) { pixels = (Pixels)obj; } else { return null; } com.sun.prism.Image platformImage = PixelUtils.pixelsToImage( pixels); ImageLoader il = Toolkit.getToolkit().loadPlatformImage( platformImage); return Toolkit.getImageAccessor().fromPlatformImage(il); } } private Image readImage() { ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; Object rawData = assistant.getData(Clipboard.RAW_IMAGE_TYPE); if (rawData == null) { Object htmlData = assistant.getData(Clipboard.HTML_TYPE); if (htmlData != null) { String url = parseIMG(htmlData); if (url != null) { try { SecurityManager sm = System.getSecurityManager(); if (sm != null) { AccessControlContext context = getAccessControlContext(); URL u = new URL(url); String protocol = u.getProtocol(); if (protocol.equalsIgnoreCase("jar")) { String file = u.getFile(); u = new URL(file); protocol = u.getProtocol(); } if (protocol.equalsIgnoreCase("file")) { FilePermission fp = new FilePermission(u.getFile(), "read"); sm.checkPermission(fp, context); } else if (protocol.equalsIgnoreCase("ftp") || protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) { int port = u.getPort(); String hoststr = (port == -1 ? u.getHost() : u.getHost() + ":" + port); SocketPermission sp = new SocketPermission(hoststr, "connect"); sm.checkPermission(sp, context); } else { PermissionHelper.checkClipboardPermission(context); } } return (new Image(url)); } catch (MalformedURLException mue) { return null; } catch (SecurityException se) { return null; } } } return null; } return convertObjectToImage(rawData); } private static final Pattern findTagIMG = Pattern.compile("IMG\\s+SRC=\\\"([^\\\"]+)\\\"", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); private String parseIMG(Object data) { if (data == null) { return null; } if ((data instanceof String) == false) { return null; } String str = (String)data; Matcher matcher = findTagIMG.matcher(str); if (matcher.find()) { return (matcher.group(1)); } else { return null; } } private boolean placeImage(final Image image) { if (image == null) { return false; } String url = image.getUrl(); if (url == null || PixelUtils.supportedFormatType(url)) { com.sun.prism.Image prismImage = (com.sun.prism.Image) Toolkit.getImageAccessor().getPlatformImage(image); Pixels pixels = PixelUtils.imageToPixels(prismImage); if (pixels != null) { systemAssistant.setData(Clipboard.RAW_IMAGE_TYPE, pixels); return true; } else { return false; } } else { systemAssistant.setData(Clipboard.URI_TYPE, url); return true; } } @Override public Set<DataFormat> getContentTypes() { Set<DataFormat> set = new HashSet<DataFormat>(); if (dataCache != null) { for (Pair<DataFormat, Object> pair : dataCache) { set.add(pair.getKey()); } return set; } ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; String[] types = assistant.getMimeTypes(); if (types == null) { return set; } for (String t: types) { if (t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) { set.add(DataFormat.IMAGE); } else if (t.equalsIgnoreCase(Clipboard.URI_TYPE)) { set.add(DataFormat.URL); } else if (t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) { set.add(DataFormat.FILES); } else if (t.equalsIgnoreCase(Clipboard.HTML_TYPE)) { set.add(DataFormat.HTML); // RT-16812 - IE puts images on the clipboard in a HTML IMG url try { //HTML header could be improperly formatted and we can get an exception here if (parseIMG(assistant.getData(Clipboard.HTML_TYPE)) != null) { set.add(DataFormat.IMAGE); } } catch (Exception ex) { //do nothing - it was just an attempt } } else { DataFormat dataFormat = DataFormat.lookupMimeType(t); if (dataFormat == null) { //The user is interested in any format. dataFormat = new DataFormat(t); } set.add(dataFormat); } } return set; } @Override public boolean hasContent(DataFormat dataFormat) { if (dataCache != null) { for (Pair<DataFormat, Object> pair : dataCache) { if (pair.getKey() == dataFormat) { return true; } } return false; } ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; String[] stypes = assistant.getMimeTypes(); if (stypes == null) { return false; } for (String t: stypes) { if (dataFormat == DataFormat.IMAGE && t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) { return true; } else if (dataFormat == DataFormat.URL && t.equalsIgnoreCase(Clipboard.URI_TYPE)) { return true; } else if (dataFormat == DataFormat.IMAGE && t.equalsIgnoreCase(Clipboard.HTML_TYPE) && parseIMG(assistant.getData(Clipboard.HTML_TYPE)) != null) { return true; } else if (dataFormat == DataFormat.FILES && t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) { return true; } DataFormat found = DataFormat.lookupMimeType(t); if (found != null && found.equals(dataFormat)) { return true; } } return false; } private static ByteBuffer prepareImage(Image image) { PixelReader pr = image.getPixelReader(); int w = (int) image.getWidth(); int h = (int) image.getHeight(); byte[] pixels = new byte[w * h * 4]; pr.getPixels(0, 0, w, h, WritablePixelFormat.getByteBgraInstance(), pixels, 0, w*4); ByteBuffer dragImageBuffer = ByteBuffer.allocate(8 + w * h * 4); dragImageBuffer.putInt(w); dragImageBuffer.putInt(h); dragImageBuffer.put(pixels); return dragImageBuffer; } private static ByteBuffer prepareOffset(double offsetX, double offsetY) { ByteBuffer dragImageOffset = ByteBuffer.allocate(8); dragImageOffset.rewind(); dragImageOffset.putInt((int) offsetX); dragImageOffset.putInt((int) offsetY); return dragImageOffset; } private boolean putContentToPeer(Pair<DataFormat, Object>... content) { systemAssistant.emptyCache(); boolean dataSet = false; // For each pair, we need to extract the DataFormat and data associated with // that pair. We then will send the data down to Glass, having done // some work for well known types (such as Image and File) in order to // adapt from FX to Glass requirements. For everything else, we just // pass down the mime type and data, for each mime type supported by // the DataFormat type. for (Pair<DataFormat, Object> pair : content) { final DataFormat dataFormat = pair.getKey(); Object data = pair.getValue(); // Images are handled specially. On Windows, the image type supported // on the clipboard is a DIB (device independent bitmap), while on Mac // it is a TIFF. Other native apps expect this entry on the clipboard // for image copy/paste. However other Java apps or FX apps (for example) // might expect the JPG bits directly, rather than the DIB / TIFF bits. // So what we do is, any IMAGE type DataFormat that comes in will be stored // in DIB / TIFF, while specific bits will also be stored (in the future). if (dataFormat == DataFormat.IMAGE) { dataSet = placeImage(convertObjectToImage(data)); } else if (dataFormat == DataFormat.URL) { // TODO Weird, but this is how Glass wants it... systemAssistant.setData(Clipboard.URI_TYPE, data); dataSet = true; } else if (dataFormat == DataFormat.RTF) { systemAssistant.setData(Clipboard.RTF_TYPE, data); dataSet = true; } else if (dataFormat == DataFormat.FILES) { // Have to convert from List<File> to String[] List<File> list = (List<File>)data; if (list.size() != 0) { String[] paths = new String[list.size()]; int i = 0; for (File f : list) { paths[i++] = f.getAbsolutePath(); } systemAssistant.setData(Clipboard.FILE_LIST_TYPE, paths); dataSet = true; } } else { if (data instanceof Serializable) { if ((dataFormat != DataFormat.PLAIN_TEXT && dataFormat != DataFormat.HTML) || !(data instanceof String)) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos); out.writeObject(data); out.close(); data = ByteBuffer.wrap(bos.toByteArray()); } catch (IOException e) { throw new IllegalArgumentException("Could not serialize the data", e); } } } else if (data instanceof InputStream) { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try (InputStream is = (InputStream)data) { // TODO: performance int i = is.read(); while (i != -1) { bout.write(i); i = is.read(); } } catch (IOException e) { throw new IllegalArgumentException("Could not serialize the data", e); } data = ByteBuffer.wrap(bout.toByteArray()); } else if (!(data instanceof ByteBuffer)) { throw new IllegalArgumentException("Only serializable " + "objects or ByteBuffer can be used as data " + "with data format " + dataFormat); } for (String mimeType : dataFormat.getIdentifiers()) { systemAssistant.setData(mimeType, data); dataSet = true; } } } // add drag image and offsets to the peer if (dragImage != null) { ByteBuffer imageBuffer = prepareImage(dragImage); ByteBuffer offsetBuffer = prepareOffset(dragOffsetX, dragOffsetY); systemAssistant.setData(Clipboard.DRAG_IMAGE, imageBuffer); systemAssistant.setData(Clipboard.DRAG_IMAGE_OFFSET, offsetBuffer); } return dataSet; } @Override public boolean putContent(Pair<DataFormat, Object>... content) { for (Pair<DataFormat, Object> pair : content) { final DataFormat format = pair.getKey(); final Object data = pair.getValue(); if (format == null) { throw new NullPointerException("Clipboard.putContent: null data format"); } if (data == null) { throw new NullPointerException("Clipboard.putContent: null data"); } } boolean dataSet = false; if (isCaching) { if (dataCache == null) { dataCache = new ArrayList<Pair<DataFormat, Object>>(content.length); } for (Pair<DataFormat, Object> pair : content) { dataCache.add(pair); dataSet = true; } } else { dataSet = putContentToPeer(content); systemAssistant.flush(); } return dataSet; } private void clearCache() { dataCache = null; transferModesCache = null; } private void clearDragView() { dragImage = null; dragOffsetX = dragOffsetY = 0; } }