/* * 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. */ final class QuantumClipboard implements TKClipboard { /** * 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 ... */ 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. */ private boolean isCaching; /** * Cache of the data used for dragboard between setting them and flushing * them to system dragboard. */ private List> dataCache; /** * Cache of the transfer modes used for dragboard between setting them and * flushing them to system dragboard. */ private Set transferModesCache; /** * 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. */ private double dragOffsetX = 0; private double dragOffsetY = 0; private static ClipboardAssistance currentDragboard; /** * 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. * @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. * @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 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 tm) { if (isCaching) { transferModesCache = tm; } final int actions = transferModesToClipboardActions(tm); systemAssistant.setSupportedActions(actions); } public static Set clipboardActionsToTransferModes(final int actions) { final Set 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 getTransferModes() { if (transferModesCache != null) { return EnumSet.copyOf(transferModesCache); } ClipboardAssistance assistant = (currentDragboard != null) ? currentDragboard : systemAssistant; final Set 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 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 list = new ArrayList(paths.length); for (int i=0; i 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 { final Permission clipboardPerm = PermissionHelper.getAccessClipboardPermission(); sm.checkPermission(clipboardPerm, 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 getContentTypes() { Set set = new HashSet(); if (dataCache != null) { for (Pair 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 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... 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 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 to String[] List list = (List)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... content) { for (Pair 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>(content.length); } for (Pair 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; } }