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.List;
  49 
  50 import javax.imageio.ImageIO;
  51 import javax.imageio.ImageTypeSpecifier;
  52 import javax.imageio.ImageWriter;
  53 import javax.imageio.spi.ImageWriterSpi;
  54 
  55 import sun.awt.datatransfer.DataTransferer;
  56 import sun.awt.datatransfer.ToolkitThreadBlockedHandler;
  57 
  58 import java.io.ByteArrayOutputStream;
  59 import java.util.stream.Stream;
  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      */
  76     private XDataTransferer() {
  77     }
  78 
  79     private static XDataTransferer transferer;
  80 
  81     static synchronized XDataTransferer getInstanceImpl() {
  82         if (transferer == null) {
  83             transferer = new XDataTransferer();
  84         }
  85         return transferer;
  86     }
  87 
  88     public String getDefaultUnicodeEncoding() {
  89         return "iso-10646-ucs-2";
  90     }
  91 
  92     public boolean isLocaleDependentTextFormat(long format) {
  93         return false;
  94     }
  95 
  96     public boolean isTextFormat(long format) {
  97         return super.isTextFormat(format)
  98             || isMimeFormat(format, "text");
  99     }
 100 
 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 
 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 
 135     public boolean isFileFormat(long format) {
 136         return format == FILE_NAME_ATOM.getAtom() ||
 137             format == DT_NET_FILE_ATOM.getAtom();
 138     }
 139 
 140     public boolean isImageFormat(long format) {
 141         return format == PNG_ATOM.getAtom() ||
 142             format == JFIF_ATOM.getAtom() ||
 143             isMimeFormat(format, "image");
 144     }
 145 
 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 
 153     protected String getNativeForFormat(long format) {
 154         return getTargetNameForAtom(format);
 155     }
 156 
 157     public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
 158         return XToolkitThreadBlockedHandler.getToolkitThreadBlockedHandler();
 159     }
 160 
 161     /**
 162      * Gets an format name for a given format (atom)
 163      */
 164     private String getTargetNameForAtom(long atom) {
 165         return XAtom.get(atom).getName();
 166     }
 167 
 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 
 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      */
 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
 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
 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<>();
 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      */
 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      */
 332     @Override
 333     public List<DataFlavor> getPlatformMappingsForNative(String nat) {
 334         List<DataFlavor> flavors = new ArrayList<>();
 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 
 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      */
 394     @Override
 395     public List<String> getPlatformMappingsForFlavor(DataFlavor df) {
 396         List<String> natives = new ArrayList<>(1);
 397 
 398         if (df == null) {
 399             return natives;
 400         }
 401 
 402         String charset = df.getParameter("charset");
 403         String baseType = df.getPrimaryType() + "/" + df.getSubType();
 404         String mimeType = baseType;
 405 
 406         if (charset != null && DataTransferer.isFlavorCharsetTextType(df)) {
 407             mimeType += ";charset=" + charset;
 408         }
 409 
 410         // Add a mapping to the MIME native whenever the representation class
 411         // doesn't require translation.
 412         if (df.getRepresentationClass() != null &&
 413             (df.isRepresentationClassInputStream() ||
 414              df.isRepresentationClassByteBuffer() ||
 415              byte[].class.equals(df.getRepresentationClass()))) {
 416             natives.add(mimeType);
 417         }
 418 
 419         if (DataFlavor.imageFlavor.equals(df)) {
 420             String[] mimeTypes = ImageIO.getWriterMIMETypes();
 421             if (mimeTypes != null) {
 422                 for (String mime : mimeTypes) {
 423                     Iterator<ImageWriter> writers = ImageIO.getImageWritersByMIMEType(mime);
 424                     while (writers.hasNext()) {
 425                         ImageWriter imageWriter = writers.next();
 426                         ImageWriterSpi writerSpi = imageWriter.getOriginatingProvider();
 427 
 428                         if (writerSpi != null &&
 429                                 writerSpi.canEncodeImage(getDefaultImageTypeSpecifier())) {
 430                             natives.add(mime);
 431                             break;
 432                         }
 433                     }
 434                 }
 435             }
 436         } else if (DataTransferer.isFlavorCharsetTextType(df)) {
 437             // stringFlavor is semantically equivalent to the standard
 438             // "text/plain" MIME type.
 439             if (DataFlavor.stringFlavor.equals(df)) {
 440                 baseType = "text/plain";
 441             }
 442 
 443             for (String encoding : DataTransferer.standardEncodings()) {
 444                 if (!encoding.equals(charset)) {
 445                     natives.add(baseType + ";charset=" + encoding);
 446                 }
 447             }
 448 
 449             // Add a MIME format without specified charset.
 450             if (!natives.contains(baseType)) {
 451                 natives.add(baseType);
 452             }
 453         }
 454 
 455         return natives;
 456     }
 457 }