1 /*
2  * Copyright (c) 2003, 2014, 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.X11;
27 
28 import java.awt.Image;
29 
30 import java.awt.datatransfer.DataFlavor;
31 import java.awt.datatransfer.Transferable;
32 import java.awt.datatransfer.UnsupportedFlavorException;
33 
34 import java.awt.image.BufferedImage;
35 import java.awt.image.ColorModel;
36 import java.awt.image.WritableRaster;
37 
38 import java.io.BufferedReader;
39 import java.io.InputStream;
40 import java.io.InputStreamReader;
41 import java.io.IOException;
42 
43 import java.net.URI;
44 import java.net.URISyntaxException;
45 
46 import java.util.ArrayList;
47 import java.util.Iterator;
48 import java.util.LinkedHashSet;
49 import java.util.List;
50 
51 import javax.imageio.ImageIO;
52 import javax.imageio.ImageTypeSpecifier;
53 import javax.imageio.ImageWriter;
54 import javax.imageio.spi.ImageWriterSpi;
55 
56 import sun.awt.datatransfer.DataTransferer;
57 import sun.awt.datatransfer.ToolkitThreadBlockedHandler;
58 
59 import java.io.ByteArrayOutputStream;
60 
61 /**
62  * Platform-specific support for the data transfer subsystem.
63  */
64 public class XDataTransferer extends DataTransferer {
65     static final XAtom FILE_NAME_ATOM = XAtom.get("FILE_NAME");
66     static final XAtom DT_NET_FILE_ATOM = XAtom.get("_DT_NETFILE");
67     static final XAtom PNG_ATOM = XAtom.get("PNG");
68     static final XAtom JFIF_ATOM = XAtom.get("JFIF");
69     static final XAtom TARGETS_ATOM = XAtom.get("TARGETS");
70     static final XAtom INCR_ATOM = XAtom.get("INCR");
71     static final XAtom MULTIPLE_ATOM = XAtom.get("MULTIPLE");
72 
73     /**
74      * Singleton constructor
75      */
XDataTransferer()76     private XDataTransferer() {
77     }
78 
79     private static XDataTransferer transferer;
80 
getInstanceImpl()81     static synchronized XDataTransferer getInstanceImpl() {
82         if (transferer == null) {
83             transferer = new XDataTransferer();
84         }
85         return transferer;
86     }
87 
getDefaultUnicodeEncoding()88     public String getDefaultUnicodeEncoding() {
89         return "iso-10646-ucs-2";
90     }
91 
isLocaleDependentTextFormat(long format)92     public boolean isLocaleDependentTextFormat(long format) {
93         return false;
94     }
95 
isTextFormat(long format)96     public boolean isTextFormat(long format) {
97         return super.isTextFormat(format)
98             || isMimeFormat(format, "text");
99     }
100 
getCharsetForTextFormat(Long lFormat)101     protected String getCharsetForTextFormat(Long lFormat) {
102         long format = lFormat.longValue();
103         if (isMimeFormat(format, "text")) {
104             String nat = getNativeForFormat(format);
105             DataFlavor df = new DataFlavor(nat, null);
106             // Ignore the charset parameter of the MIME type if the subtype
107             // doesn't support charset.
108             if (!DataTransferer.doesSubtypeSupportCharset(df)) {
109                 return null;
110             }
111             String charset = df.getParameter("charset");
112             if (charset != null) {
113                 return charset;
114             }
115         }
116         return super.getCharsetForTextFormat(lFormat);
117     }
118 
isURIListFormat(long format)119     protected boolean isURIListFormat(long format) {
120         String nat = getNativeForFormat(format);
121         if (nat == null) {
122             return false;
123         }
124         try {
125             DataFlavor df = new DataFlavor(nat);
126             if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) {
127                 return true;
128             }
129         } catch (Exception e) {
130             // Not a MIME format.
131         }
132         return false;
133     }
134 
isFileFormat(long format)135     public boolean isFileFormat(long format) {
136         return format == FILE_NAME_ATOM.getAtom() ||
137             format == DT_NET_FILE_ATOM.getAtom();
138     }
139 
isImageFormat(long format)140     public boolean isImageFormat(long format) {
141         return format == PNG_ATOM.getAtom() ||
142             format == JFIF_ATOM.getAtom() ||
143             isMimeFormat(format, "image");
144     }
145 
getFormatForNativeAsLong(String str)146     protected Long getFormatForNativeAsLong(String str) {
147         // Just get the atom. If it has already been retrived
148         // once, we'll get a copy so this should be very fast.
149         long atom = XAtom.get(str).getAtom();
150         return Long.valueOf(atom);
151     }
152 
getNativeForFormat(long format)153     protected String getNativeForFormat(long format) {
154         return getTargetNameForAtom(format);
155     }
156 
getToolkitThreadBlockedHandler()157     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
158         return XToolkitThreadBlockedHandler.getToolkitThreadBlockedHandler();
159     }
160 
161     /**
162      * Gets an format name for a given format (atom)
163      */
getTargetNameForAtom(long atom)164     private String getTargetNameForAtom(long atom) {
165         return XAtom.get(atom).getName();
166     }
167 
imageToPlatformBytes(Image image, long format)168     protected byte[] imageToPlatformBytes(Image image, long format)
169       throws IOException {
170         String mimeType = null;
171         if (format == PNG_ATOM.getAtom()) {
172             mimeType = "image/png";
173         } else if (format == JFIF_ATOM.getAtom()) {
174             mimeType = "image/jpeg";
175         } else {
176             // Check if an image MIME format.
177             try {
178                 String nat = getNativeForFormat(format);
179                 DataFlavor df = new DataFlavor(nat);
180                 String primaryType = df.getPrimaryType();
181                 if ("image".equals(primaryType)) {
182                     mimeType = df.getPrimaryType() + "/" + df.getSubType();
183                 }
184             } catch (Exception e) {
185                 // Not an image MIME format.
186             }
187         }
188         if (mimeType != null) {
189             return imageToStandardBytes(image, mimeType);
190         } else {
191             String nativeFormat = getNativeForFormat(format);
192             throw new IOException("Translation to " + nativeFormat +
193                                   " is not supported.");
194         }
195     }
196 
convertFileListToBytes(ArrayList<String> fileList)197     protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList)
198         throws IOException
199     {
200         ByteArrayOutputStream bos = new ByteArrayOutputStream();
201         for (int i = 0; i < fileList.size(); i++)
202         {
203                byte[] bytes = fileList.get(i).getBytes();
204                if (i != 0) bos.write(0);
205                bos.write(bytes, 0, bytes.length);
206         }
207         return bos;
208     }
209 
210     /**
211      * Translates either a byte array or an input stream which contain
212      * platform-specific image data in the given format into an Image.
213      */
platformImageBytesToImage( byte[] bytes, long format)214     protected Image platformImageBytesToImage(
215         byte[] bytes, long format) throws IOException
216     {
217         String mimeType = null;
218         if (format == PNG_ATOM.getAtom()) {
219             mimeType = "image/png";
220         } else if (format == JFIF_ATOM.getAtom()) {
221             mimeType = "image/jpeg";
222         } else {
223             // Check if an image MIME format.
224             try {
225                 String nat = getNativeForFormat(format);
226                 DataFlavor df = new DataFlavor(nat);
227                 String primaryType = df.getPrimaryType();
228                 if ("image".equals(primaryType)) {
229                     mimeType = df.getPrimaryType() + "/" + df.getSubType();
230                 }
231             } catch (Exception e) {
232                 // Not an image MIME format.
233             }
234         }
235         if (mimeType != null) {
236             return standardImageBytesToImage(bytes, mimeType);
237         } else {
238             String nativeFormat = getNativeForFormat(format);
239             throw new IOException("Translation from " + nativeFormat +
240                                   " is not supported.");
241         }
242     }
243 
244     @Override
dragQueryFile(byte[] bytes)245     protected String[] dragQueryFile(byte[] bytes) {
246         XToolkit.awtLock();
247         try {
248             return XlibWrapper.XTextPropertyToStringList(bytes,
249                                                          XAtom.get("STRING").getAtom());
250         } finally {
251             XToolkit.awtUnlock();
252         }
253     }
254 
255     @Override
dragQueryURIs(InputStream stream, long format, Transferable localeTransferable)256     protected URI[] dragQueryURIs(InputStream stream,
257                                   long format,
258                                   Transferable localeTransferable)
259       throws IOException {
260 
261         String charset = null;
262         if (localeTransferable != null &&
263             isLocaleDependentTextFormat(format) &&
264             localeTransferable.isDataFlavorSupported(javaTextEncodingFlavor)) {
265             try {
266                 charset = new String(
267                     (byte[])localeTransferable.getTransferData(javaTextEncodingFlavor),
268                     "UTF-8"
269                 );
270             } catch (UnsupportedFlavorException cannotHappen) {
271             }
272         } else {
273             charset = getCharsetForTextFormat(format);
274         }
275         if (charset == null) {
276             // Only happens when we have a custom text type.
277             charset = getDefaultTextCharset();
278         }
279 
280         BufferedReader reader = null;
281         try {
282             reader = new BufferedReader(new InputStreamReader(stream, charset));
283             String line;
284             ArrayList<URI> uriList = new ArrayList<URI>();
285             URI uri;
286             while ((line = reader.readLine()) != null) {
287                 try {
288                     uri = new URI(line);
289                 } catch (URISyntaxException uriSyntaxException) {
290                     throw new IOException(uriSyntaxException);
291                 }
292                 uriList.add(uri);
293             }
294             return uriList.toArray(new URI[uriList.size()]);
295         } finally {
296             if (reader != null)
297                 reader.close();
298         }
299     }
300 
301     /**
302      * Returns true if and only if the name of the specified format Atom
303      * constitutes a valid MIME type with the specified primary type.
304      */
isMimeFormat(long format, String primaryType)305     private boolean isMimeFormat(long format, String primaryType) {
306         String nat = getNativeForFormat(format);
307 
308         if (nat == null) {
309             return false;
310         }
311 
312         try {
313             DataFlavor df = new DataFlavor(nat);
314             if (primaryType.equals(df.getPrimaryType())) {
315                 return true;
316             }
317         } catch (Exception e) {
318             // Not a MIME format.
319         }
320 
321         return false;
322     }
323 
324     /*
325      * The XDnD protocol prescribes that the Atoms used as targets for data
326      * transfer should have string names that represent the corresponding MIME
327      * types.
328      * To meet this requirement we check if the passed native format constitutes
329      * a valid MIME and return a list of flavors to which the data in this MIME
330      * type can be translated by the Data Transfer subsystem.
331      */
getPlatformMappingsForNative(String nat)332     public LinkedHashSet<DataFlavor> getPlatformMappingsForNative(String nat) {
333         LinkedHashSet<DataFlavor> flavors = new LinkedHashSet<>();
334 
335 
336         if (nat == null) {
337             return flavors;
338         }
339 
340         DataFlavor df = null;
341 
342         try {
343             df = new DataFlavor(nat);
344         } catch (Exception e) {
345             // The string doesn't constitute a valid MIME type.
346             return flavors;
347         }
348 
349         DataFlavor value = df;
350         final String primaryType = df.getPrimaryType();
351         final String baseType = primaryType + "/" + df.getSubType();
352 
353         // For text formats we map natives to MIME strings instead of data
354         // flavors to enable dynamic text native-to-flavor mapping generation.
355         // See SystemFlavorMap.getFlavorsForNative() for details.
356         if ("image".equals(primaryType)) {
357             Iterator readers = ImageIO.getImageReadersByMIMEType(baseType);
358             if (readers.hasNext()) {
359                 flavors.add(DataFlavor.imageFlavor);
360             }
361         }
362 
363         flavors.add(value);
364 
365         return flavors;
366     }
367 
368     private static ImageTypeSpecifier defaultSpecifier = null;
369 
getDefaultImageTypeSpecifier()370     private ImageTypeSpecifier getDefaultImageTypeSpecifier() {
371         if (defaultSpecifier == null) {
372             ColorModel model = ColorModel.getRGBdefault();
373             WritableRaster raster =
374                 model.createCompatibleWritableRaster(10, 10);
375 
376             BufferedImage bufferedImage =
377                 new BufferedImage(model, raster, model.isAlphaPremultiplied(),
378                                   null);
379 
380             defaultSpecifier = new ImageTypeSpecifier(bufferedImage);
381         }
382 
383         return defaultSpecifier;
384     }
385 
386     /*
387      * The XDnD protocol prescribes that the Atoms used as targets for data
388      * transfer should have string names that represent the corresponding MIME
389      * types.
390      * To meet this requirement we return a list of formats that represent
391      * MIME types to which the data in this flavor can be translated by the Data
392      * Transfer subsystem.
393      */
getPlatformMappingsForFlavor(DataFlavor df)394     public LinkedHashSet<String> getPlatformMappingsForFlavor(DataFlavor df) {
395         LinkedHashSet<String> natives = new LinkedHashSet<>(1);
396 
397         if (df == null) {
398             return natives;
399         }
400 
401         String charset = df.getParameter("charset");
402         String baseType = df.getPrimaryType() + "/" + df.getSubType();
403         String mimeType = baseType;
404 
405         if (charset != null && DataTransferer.isFlavorCharsetTextType(df)) {
406             mimeType += ";charset=" + charset;
407         }
408 
409         // Add a mapping to the MIME native whenever the representation class
410         // doesn't require translation.
411         if (df.getRepresentationClass() != null &&
412             (df.isRepresentationClassInputStream() ||
413              df.isRepresentationClassByteBuffer() ||
414              byte[].class.equals(df.getRepresentationClass()))) {
415             natives.add(mimeType);
416         }
417 
418         if (DataFlavor.imageFlavor.equals(df)) {
419             String[] mimeTypes = ImageIO.getWriterMIMETypes();
420             if (mimeTypes != null) {
421                 for (int i = 0; i < mimeTypes.length; i++) {
422                     Iterator writers =
423                         ImageIO.getImageWritersByMIMEType(mimeTypes[i]);
424 
425                     while (writers.hasNext()) {
426                         ImageWriter imageWriter = (ImageWriter)writers.next();
427                         ImageWriterSpi writerSpi =
428                             imageWriter.getOriginatingProvider();
429 
430                         if (writerSpi != null &&
431                             writerSpi.canEncodeImage(getDefaultImageTypeSpecifier())) {
432                             natives.add(mimeTypes[i]);
433                             break;
434                         }
435                     }
436                 }
437             }
438         } else if (DataTransferer.isFlavorCharsetTextType(df)) {
439             // stringFlavor is semantically equivalent to the standard
440             // "text/plain" MIME type.
441             if (DataFlavor.stringFlavor.equals(df)) {
442                 baseType = "text/plain";
443             }
444 
445             for (String encoding : DataTransferer.standardEncodings()) {
446                 if (!encoding.equals(charset)) {
447                     natives.add(baseType + ";charset=" + encoding);
448                 }
449             }
450 
451             // Add a MIME format without specified charset.
452             if (!natives.contains(baseType)) {
453                 natives.add(baseType);
454             }
455         }
456 
457         return natives;
458     }
459 }
460