1 /*
   2  * Copyright (c) 1997, 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 java.awt.datatransfer;
  27 
  28 import sun.datatransfer.DataFlavorUtil;
  29 import sun.datatransfer.DesktopDatatransferService;
  30 
  31 import java.io.BufferedReader;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.InputStreamReader;
  35 import java.lang.ref.SoftReference;
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.LinkedHashSet;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.Objects;
  44 import java.util.Set;
  45 
  46 /**
  47  * The SystemFlavorMap is a configurable map between "natives" (Strings), which
  48  * correspond to platform-specific data formats, and "flavors" (DataFlavors),
  49  * which correspond to platform-independent MIME types. This mapping is used
  50  * by the data transfer subsystem to transfer data between Java and native
  51  * applications, and between Java applications in separate VMs.
  52  *
  53  * @since 1.2
  54  */
  55 public final class SystemFlavorMap implements FlavorMap, FlavorTable {
  56 
  57     /**
  58      * Constant prefix used to tag Java types converted to native platform
  59      * type.
  60      */
  61     private static String JavaMIME = "JAVA_DATAFLAVOR:";
  62 
  63     private static final Object FLAVOR_MAP_KEY = new Object();
  64 
  65     /**
  66      * The list of valid, decoded text flavor representation classes, in order
  67      * from best to worst.
  68      */
  69     private static final String[] UNICODE_TEXT_CLASSES = {
  70         "java.io.Reader", "java.lang.String", "java.nio.CharBuffer", "\"[C\""
  71     };
  72 
  73     /**
  74      * The list of valid, encoded text flavor representation classes, in order
  75      * from best to worst.
  76      */
  77     private static final String[] ENCODED_TEXT_CLASSES = {
  78         "java.io.InputStream", "java.nio.ByteBuffer", "\"[B\""
  79     };
  80 
  81     /**
  82      * A String representing text/plain MIME type.
  83      */
  84     private static final String TEXT_PLAIN_BASE_TYPE = "text/plain";
  85 
  86     /**
  87      * A String representing text/html MIME type.
  88      */
  89     private static final String HTML_TEXT_BASE_TYPE = "text/html";
  90 
  91     /**
  92      * Maps native Strings to Lists of DataFlavors (or base type Strings for
  93      * text DataFlavors).
  94      * Do not use the field directly, use getNativeToFlavor() instead.
  95      */
  96     private final Map<String, LinkedHashSet<DataFlavor>> nativeToFlavor = new HashMap<>();
  97 
  98     /**
  99      * Accessor to nativeToFlavor map.  Since we use lazy initialization we must
 100      * use this accessor instead of direct access to the field which may not be
 101      * initialized yet.  This method will initialize the field if needed.
 102      *
 103      * @return nativeToFlavor
 104      */
 105     private Map<String, LinkedHashSet<DataFlavor>> getNativeToFlavor() {
 106         if (!isMapInitialized) {
 107             initSystemFlavorMap();
 108         }
 109         return nativeToFlavor;
 110     }
 111 
 112     /**
 113      * Maps DataFlavors (or base type Strings for text DataFlavors) to Lists of
 114      * native Strings.
 115      * Do not use the field directly, use getFlavorToNative() instead.
 116      */
 117     private final Map<DataFlavor, LinkedHashSet<String>> flavorToNative = new HashMap<>();
 118 
 119     /**
 120      * Accessor to flavorToNative map.  Since we use lazy initialization we must
 121      * use this accessor instead of direct access to the field which may not be
 122      * initialized yet.  This method will initialize the field if needed.
 123      *
 124      * @return flavorToNative
 125      */
 126     private synchronized Map<DataFlavor, LinkedHashSet<String>> getFlavorToNative() {
 127         if (!isMapInitialized) {
 128             initSystemFlavorMap();
 129         }
 130         return flavorToNative;
 131     }
 132 
 133     /**
 134      * Maps a text DataFlavor primary mime-type to the native. Used only to store
 135      * standard mappings registered in the flavormap.properties
 136      * Do not use this field directly, use getTextTypeToNative() instead.
 137      */
 138     private Map<String, LinkedHashSet<String>> textTypeToNative = new HashMap<>();
 139 
 140     /**
 141      * Shows if the object has been initialized.
 142      */
 143     private boolean isMapInitialized = false;
 144 
 145     /**
 146      * An accessor to textTypeToNative map.  Since we use lazy initialization we
 147      * must use this accessor instead of direct access to the field which may not
 148      * be initialized yet. This method will initialize the field if needed.
 149      *
 150      * @return textTypeToNative
 151      */
 152     private synchronized Map<String, LinkedHashSet<String>> getTextTypeToNative() {
 153         if (!isMapInitialized) {
 154             initSystemFlavorMap();
 155             // From this point the map should not be modified
 156             textTypeToNative = Collections.unmodifiableMap(textTypeToNative);
 157         }
 158         return textTypeToNative;
 159     }
 160 
 161     /**
 162      * Caches the result of getNativesForFlavor(). Maps DataFlavors to
 163      * SoftReferences which reference LinkedHashSet of String natives.
 164      */
 165     private final SoftCache<DataFlavor, String> nativesForFlavorCache = new SoftCache<>();
 166 
 167     /**
 168      * Caches the result getFlavorsForNative(). Maps String natives to
 169      * SoftReferences which reference LinkedHashSet of DataFlavors.
 170      */
 171     private final SoftCache<String, DataFlavor> flavorsForNativeCache = new SoftCache<>();
 172 
 173     /**
 174      * Dynamic mapping generation used for text mappings should not be applied
 175      * to the DataFlavors and String natives for which the mappings have been
 176      * explicitly specified with setFlavorsForNative() or
 177      * setNativesForFlavor(). This keeps all such keys.
 178      */
 179     private Set<Object> disabledMappingGenerationKeys = new HashSet<>();
 180 
 181     /**
 182      * Returns the default FlavorMap for this thread's ClassLoader.
 183      *
 184      * @return the default FlavorMap for this thread's ClassLoader
 185      */
 186     public static FlavorMap getDefaultFlavorMap() {
 187         return DataFlavorUtil.getDesktopService().getFlavorMap(SystemFlavorMap::new);
 188     }
 189 
 190     private SystemFlavorMap() {
 191     }
 192 
 193     /**
 194      * Initializes a SystemFlavorMap by reading flavormap.properties
 195      * For thread-safety must be called under lock on this.
 196      */
 197     private void initSystemFlavorMap() {
 198         if (isMapInitialized) {
 199             return;
 200         }
 201         isMapInitialized = true;
 202 
 203         InputStream is = SystemFlavorMap.class.getResourceAsStream("/sun/datatransfer/resources/flavormap.properties");
 204         if (is == null) {
 205             throw new InternalError("Default flavor mapping not found");
 206         }
 207 
 208         try (InputStreamReader isr = new InputStreamReader(is);
 209              BufferedReader reader = new BufferedReader(isr)) {
 210             String line;
 211             while ((line = reader.readLine()) != null) {
 212                 line = line.trim();
 213                 if (line.startsWith("#") || line.isEmpty()) continue;
 214                 while (line.endsWith("\\")) {
 215                     line = line.substring(0, line.length() - 1) + reader.readLine().trim();
 216                 }
 217                 int delimiterPosition = line.indexOf('=');
 218                 String key = line.substring(0, delimiterPosition).replaceAll("\\ ", " ");
 219                 String[] values = line.substring(delimiterPosition + 1, line.length()).split(",");
 220                 for (String value : values) {
 221                     try {
 222                         value = loadConvert(value);
 223                         MimeType mime = new MimeType(value);
 224                         if ("text".equals(mime.getPrimaryType())) {
 225                             String charset = mime.getParameter("charset");
 226                             if (DataFlavorUtil.doesSubtypeSupportCharset(mime.getSubType(), charset))
 227                             {
 228                                 // We need to store the charset and eoln
 229                                 // parameters, if any, so that the
 230                                 // DataTransferer will have this information
 231                                 // for conversion into the native format.
 232                                 DesktopDatatransferService desktopService =
 233                                         DataFlavorUtil.getDesktopService();
 234                                 if (desktopService.isDesktopPresent()) {
 235                                     desktopService.registerTextFlavorProperties(
 236                                             key, charset,
 237                                             mime.getParameter("eoln"),
 238                                             mime.getParameter("terminators"));
 239                                 }
 240                             }
 241 
 242                             // But don't store any of these parameters in the
 243                             // DataFlavor itself for any text natives (even
 244                             // non-charset ones). The SystemFlavorMap will
 245                             // synthesize the appropriate mappings later.
 246                             mime.removeParameter("charset");
 247                             mime.removeParameter("class");
 248                             mime.removeParameter("eoln");
 249                             mime.removeParameter("terminators");
 250                             value = mime.toString();
 251                         }
 252                     } catch (MimeTypeParseException e) {
 253                         e.printStackTrace();
 254                         continue;
 255                     }
 256 
 257                     DataFlavor flavor;
 258                     try {
 259                         flavor = new DataFlavor(value);
 260                     } catch (Exception e) {
 261                         try {
 262                             flavor = new DataFlavor(value, null);
 263                         } catch (Exception ee) {
 264                             ee.printStackTrace();
 265                             continue;
 266                         }
 267                     }
 268 
 269                     final LinkedHashSet<DataFlavor> dfs = new LinkedHashSet<>();
 270                     dfs.add(flavor);
 271 
 272                     if ("text".equals(flavor.getPrimaryType())) {
 273                         dfs.addAll(convertMimeTypeToDataFlavors(value));
 274                         store(flavor.mimeType.getBaseType(), key, getTextTypeToNative());
 275                     }
 276 
 277                     for (DataFlavor df : dfs) {
 278                         store(df, key, getFlavorToNative());
 279                         store(key, df, getNativeToFlavor());
 280                     }
 281                 }
 282             }
 283         } catch (IOException e) {
 284             throw new InternalError("Error reading default flavor mapping", e);
 285         }
 286     }
 287 
 288     // Copied from java.util.Properties
 289     private static String loadConvert(String theString) {
 290         char aChar;
 291         int len = theString.length();
 292         StringBuilder outBuffer = new StringBuilder(len);
 293 
 294         for (int x = 0; x < len; ) {
 295             aChar = theString.charAt(x++);
 296             if (aChar == '\\') {
 297                 aChar = theString.charAt(x++);
 298                 if (aChar == 'u') {
 299                     // Read the xxxx
 300                     int value = 0;
 301                     for (int i = 0; i < 4; i++) {
 302                         aChar = theString.charAt(x++);
 303                         switch (aChar) {
 304                             case '0': case '1': case '2': case '3': case '4':
 305                             case '5': case '6': case '7': case '8': case '9': {
 306                                 value = (value << 4) + aChar - '0';
 307                                 break;
 308                             }
 309                             case 'a': case 'b': case 'c':
 310                             case 'd': case 'e': case 'f': {
 311                                 value = (value << 4) + 10 + aChar - 'a';
 312                                 break;
 313                             }
 314                             case 'A': case 'B': case 'C':
 315                             case 'D': case 'E': case 'F': {
 316                                 value = (value << 4) + 10 + aChar - 'A';
 317                                 break;
 318                             }
 319                             default: {
 320                                 throw new IllegalArgumentException(
 321                                         "Malformed \\uxxxx encoding.");
 322                             }
 323                         }
 324                     }
 325                     outBuffer.append((char)value);
 326                 } else {
 327                     if (aChar == 't') {
 328                         aChar = '\t';
 329                     } else if (aChar == 'r') {
 330                         aChar = '\r';
 331                     } else if (aChar == 'n') {
 332                         aChar = '\n';
 333                     } else if (aChar == 'f') {
 334                         aChar = '\f';
 335                     }
 336                     outBuffer.append(aChar);
 337                 }
 338             } else {
 339                 outBuffer.append(aChar);
 340             }
 341         }
 342         return outBuffer.toString();
 343     }
 344 
 345     /**
 346      * Stores the listed object under the specified hash key in map. Unlike a
 347      * standard map, the listed object will not replace any object already at
 348      * the appropriate Map location, but rather will be appended to a List
 349      * stored in that location.
 350      */
 351     private <H, L> void store(H hashed, L listed, Map<H, LinkedHashSet<L>> map) {
 352         LinkedHashSet<L> list = map.get(hashed);
 353         if (list == null) {
 354             list = new LinkedHashSet<>(1);
 355             map.put(hashed, list);
 356         }
 357         if (!list.contains(listed)) {
 358             list.add(listed);
 359         }
 360     }
 361 
 362     /**
 363      * Semantically equivalent to 'nativeToFlavor.get(nat)'. This method
 364      * handles the case where 'nat' is not found in 'nativeToFlavor'. In that
 365      * case, a new DataFlavor is synthesized, stored, and returned, if and
 366      * only if the specified native is encoded as a Java MIME type.
 367      */
 368     private LinkedHashSet<DataFlavor> nativeToFlavorLookup(String nat) {
 369         LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(nat);
 370 
 371         if (nat != null && !disabledMappingGenerationKeys.contains(nat)) {
 372             DesktopDatatransferService desktopService = DataFlavorUtil.getDesktopService();
 373             if (desktopService.isDesktopPresent()) {
 374                 LinkedHashSet<DataFlavor> platformFlavors =
 375                         desktopService.getPlatformMappingsForNative(nat);
 376                 if (!platformFlavors.isEmpty()) {
 377                     if (flavors != null) {
 378                         // Prepending the platform-specific mappings ensures
 379                         // that the flavors added with
 380                         // addFlavorForUnencodedNative() are at the end of
 381                         // list.
 382                         platformFlavors.addAll(flavors);
 383                     }
 384                     flavors = platformFlavors;
 385                 }
 386             }
 387         }
 388 
 389         if (flavors == null && isJavaMIMEType(nat)) {
 390             String decoded = decodeJavaMIMEType(nat);
 391             DataFlavor flavor = null;
 392 
 393             try {
 394                 flavor = new DataFlavor(decoded);
 395             } catch (Exception e) {
 396                 System.err.println("Exception \"" + e.getClass().getName() +
 397                                    ": " + e.getMessage()  +
 398                                    "\"while constructing DataFlavor for: " +
 399                                    decoded);
 400             }
 401 
 402             if (flavor != null) {
 403                 flavors = new LinkedHashSet<>(1);
 404                 getNativeToFlavor().put(nat, flavors);
 405                 flavors.add(flavor);
 406                 flavorsForNativeCache.remove(nat);
 407 
 408                 LinkedHashSet<String> natives = getFlavorToNative().get(flavor);
 409                 if (natives == null) {
 410                     natives = new LinkedHashSet<>(1);
 411                     getFlavorToNative().put(flavor, natives);
 412                 }
 413                 natives.add(nat);
 414                 nativesForFlavorCache.remove(flavor);
 415             }
 416         }
 417 
 418         return (flavors != null) ? flavors : new LinkedHashSet<>(0);
 419     }
 420 
 421     /**
 422      * Semantically equivalent to 'flavorToNative.get(flav)'. This method
 423      * handles the case where 'flav' is not found in 'flavorToNative' depending
 424      * on the value of passes 'synthesize' parameter. If 'synthesize' is
 425      * SYNTHESIZE_IF_NOT_FOUND a native is synthesized, stored, and returned by
 426      * encoding the DataFlavor's MIME type. Otherwise an empty List is returned
 427      * and 'flavorToNative' remains unaffected.
 428      */
 429     private LinkedHashSet<String> flavorToNativeLookup(final DataFlavor flav,
 430                                                        final boolean synthesize) {
 431 
 432         LinkedHashSet<String> natives = getFlavorToNative().get(flav);
 433 
 434         if (flav != null && !disabledMappingGenerationKeys.contains(flav)) {
 435             DesktopDatatransferService desktopService = DataFlavorUtil.getDesktopService();
 436             if (desktopService.isDesktopPresent()) {
 437                 LinkedHashSet<String> platformNatives =
 438                         desktopService.getPlatformMappingsForFlavor(flav);
 439                 if (!platformNatives.isEmpty()) {
 440                     if (natives != null) {
 441                         // Prepend the platform-specific mappings to ensure
 442                         // that the natives added with
 443                         // addUnencodedNativeForFlavor() are at the end of
 444                         // list.
 445                         platformNatives.addAll(natives);
 446                     }
 447                     natives = platformNatives;
 448                 }
 449             }
 450         }
 451 
 452         if (natives == null) {
 453             if (synthesize) {
 454                 String encoded = encodeDataFlavor(flav);
 455                 natives = new LinkedHashSet<>(1);
 456                 getFlavorToNative().put(flav, natives);
 457                 natives.add(encoded);
 458 
 459                 LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(encoded);
 460                 if (flavors == null) {
 461                     flavors = new LinkedHashSet<>(1);
 462                     getNativeToFlavor().put(encoded, flavors);
 463                 }
 464                 flavors.add(flav);
 465 
 466                 nativesForFlavorCache.remove(flav);
 467                 flavorsForNativeCache.remove(encoded);
 468             } else {
 469                 natives = new LinkedHashSet<>(0);
 470             }
 471         }
 472 
 473         return new LinkedHashSet<>(natives);
 474     }
 475 
 476     /**
 477      * Returns a <code>List</code> of <code>String</code> natives to which the
 478      * specified <code>DataFlavor</code> can be translated by the data transfer
 479      * subsystem. The <code>List</code> will be sorted from best native to
 480      * worst. That is, the first native will best reflect data in the specified
 481      * flavor to the underlying native platform.
 482      * <p>
 483      * If the specified <code>DataFlavor</code> is previously unknown to the
 484      * data transfer subsystem and the data transfer subsystem is unable to
 485      * translate this <code>DataFlavor</code> to any existing native, then
 486      * invoking this method will establish a
 487      * mapping in both directions between the specified <code>DataFlavor</code>
 488      * and an encoded version of its MIME type as its native.
 489      *
 490      * @param flav the <code>DataFlavor</code> whose corresponding natives
 491      *        should be returned. If <code>null</code> is specified, all
 492      *        natives currently known to the data transfer subsystem are
 493      *        returned in a non-deterministic order.
 494      * @return a <code>java.util.List</code> of <code>java.lang.String</code>
 495      *         objects which are platform-specific representations of platform-
 496      *         specific data formats
 497      *
 498      * @see #encodeDataFlavor
 499      * @since 1.4
 500      */
 501     @Override
 502     public synchronized List<String> getNativesForFlavor(DataFlavor flav) {
 503         LinkedHashSet<String> retval = nativesForFlavorCache.check(flav);
 504         if (retval != null) {
 505             return new ArrayList<>(retval);
 506         }
 507 
 508         if (flav == null) {
 509             retval = new LinkedHashSet<>(getNativeToFlavor().keySet());
 510         } else if (disabledMappingGenerationKeys.contains(flav)) {
 511             // In this case we shouldn't synthesize a native for this flavor,
 512             // since its mappings were explicitly specified.
 513             retval = flavorToNativeLookup(flav, false);
 514         } else if (DataFlavorUtil.isFlavorCharsetTextType(flav)) {
 515             retval = new LinkedHashSet<>(0);
 516 
 517             // For text/* flavors, flavor-to-native mappings specified in
 518             // flavormap.properties are stored per flavor's base type.
 519             if ("text".equals(flav.getPrimaryType())) {
 520                 LinkedHashSet<String> textTypeNatives =
 521                         getTextTypeToNative().get(flav.mimeType.getBaseType());
 522                 if (textTypeNatives != null) {
 523                     retval.addAll(textTypeNatives);
 524                 }
 525             }
 526 
 527             // Also include text/plain natives, but don't duplicate Strings
 528             LinkedHashSet<String> textTypeNatives =
 529                     getTextTypeToNative().get(TEXT_PLAIN_BASE_TYPE);
 530             if (textTypeNatives != null) {
 531                 retval.addAll(textTypeNatives);
 532             }
 533 
 534             if (retval.isEmpty()) {
 535                 retval = flavorToNativeLookup(flav, true);
 536             } else {
 537                 // In this branch it is guaranteed that natives explicitly
 538                 // listed for flav's MIME type were added with
 539                 // addUnencodedNativeForFlavor(), so they have lower priority.
 540                 retval.addAll(flavorToNativeLookup(flav, false));
 541             }
 542         } else if (DataFlavorUtil.isFlavorNoncharsetTextType(flav)) {
 543             retval = getTextTypeToNative().get(flav.mimeType.getBaseType());
 544 
 545             if (retval == null || retval.isEmpty()) {
 546                 retval = flavorToNativeLookup(flav, true);
 547             } else {
 548                 // In this branch it is guaranteed that natives explicitly
 549                 // listed for flav's MIME type were added with
 550                 // addUnencodedNativeForFlavor(), so they have lower priority.
 551                 retval.addAll(flavorToNativeLookup(flav, false));
 552             }
 553         } else {
 554             retval = flavorToNativeLookup(flav, true);
 555         }
 556 
 557         nativesForFlavorCache.put(flav, retval);
 558         // Create a copy, because client code can modify the returned list.
 559         return new ArrayList<>(retval);
 560     }
 561 
 562     /**
 563      * Returns a <code>List</code> of <code>DataFlavor</code>s to which the
 564      * specified <code>String</code> native can be translated by the data
 565      * transfer subsystem. The <code>List</code> will be sorted from best
 566      * <code>DataFlavor</code> to worst. That is, the first
 567      * <code>DataFlavor</code> will best reflect data in the specified
 568      * native to a Java application.
 569      * <p>
 570      * If the specified native is previously unknown to the data transfer
 571      * subsystem, and that native has been properly encoded, then invoking this
 572      * method will establish a mapping in both directions between the specified
 573      * native and a <code>DataFlavor</code> whose MIME type is a decoded
 574      * version of the native.
 575      * <p>
 576      * If the specified native is not a properly encoded native and the
 577      * mappings for this native have not been altered with
 578      * <code>setFlavorsForNative</code>, then the contents of the
 579      * <code>List</code> is platform dependent, but <code>null</code>
 580      * cannot be returned.
 581      *
 582      * @param nat the native whose corresponding <code>DataFlavor</code>s
 583      *        should be returned. If <code>null</code> is specified, all
 584      *        <code>DataFlavor</code>s currently known to the data transfer
 585      *        subsystem are returned in a non-deterministic order.
 586      * @return a <code>java.util.List</code> of <code>DataFlavor</code>
 587      *         objects into which platform-specific data in the specified,
 588      *         platform-specific native can be translated
 589      *
 590      * @see #encodeJavaMIMEType
 591      * @since 1.4
 592      */
 593     @Override
 594     public synchronized List<DataFlavor> getFlavorsForNative(String nat) {
 595         LinkedHashSet<DataFlavor> returnValue = flavorsForNativeCache.check(nat);
 596         if (returnValue != null) {
 597             return new ArrayList<>(returnValue);
 598         } else {
 599             returnValue = new LinkedHashSet<>();
 600         }
 601 
 602         if (nat == null) {
 603             for (String n : getNativesForFlavor(null)) {
 604                 returnValue.addAll(getFlavorsForNative(n));
 605             }
 606         } else {
 607             final LinkedHashSet<DataFlavor> flavors = nativeToFlavorLookup(nat);
 608             if (disabledMappingGenerationKeys.contains(nat)) {
 609                 return new ArrayList<>(flavors);
 610             }
 611 
 612             final LinkedHashSet<DataFlavor> flavorsWithSynthesized =
 613                     nativeToFlavorLookup(nat);
 614 
 615             for (DataFlavor df : flavorsWithSynthesized) {
 616                 returnValue.add(df);
 617                 if ("text".equals(df.getPrimaryType())) {
 618                     String baseType = df.mimeType.getBaseType();
 619                     returnValue.addAll(convertMimeTypeToDataFlavors(baseType));
 620                 }
 621             }
 622         }
 623         flavorsForNativeCache.put(nat, returnValue);
 624         return new ArrayList<>(returnValue);
 625     }
 626 
 627     @SuppressWarnings("deprecation")
 628     private static Set<DataFlavor> convertMimeTypeToDataFlavors(
 629         final String baseType) {
 630 
 631         final Set<DataFlavor> returnValue = new LinkedHashSet<>();
 632 
 633         String subType = null;
 634 
 635         try {
 636             final MimeType mimeType = new MimeType(baseType);
 637             subType = mimeType.getSubType();
 638         } catch (MimeTypeParseException mtpe) {
 639             // Cannot happen, since we checked all mappings
 640             // on load from flavormap.properties.
 641         }
 642 
 643         if (DataFlavorUtil.doesSubtypeSupportCharset(subType, null)) {
 644             if (TEXT_PLAIN_BASE_TYPE.equals(baseType))
 645             {
 646                 returnValue.add(DataFlavor.stringFlavor);
 647             }
 648 
 649             for (String unicodeClassName : UNICODE_TEXT_CLASSES) {
 650                 final String mimeType = baseType + ";charset=Unicode;class=" +
 651                                             unicodeClassName;
 652 
 653                 final LinkedHashSet<String> mimeTypes =
 654                     handleHtmlMimeTypes(baseType, mimeType);
 655                 for (String mt : mimeTypes) {
 656                     DataFlavor toAdd = null;
 657                     try {
 658                         toAdd = new DataFlavor(mt);
 659                     } catch (ClassNotFoundException cannotHappen) {
 660                     }
 661                     returnValue.add(toAdd);
 662                 }
 663             }
 664 
 665             for (String charset : DataFlavorUtil.standardEncodings()) {
 666 
 667                 for (String encodedTextClass : ENCODED_TEXT_CLASSES) {
 668                     final String mimeType =
 669                             baseType + ";charset=" + charset +
 670                             ";class=" + encodedTextClass;
 671 
 672                     final LinkedHashSet<String> mimeTypes =
 673                         handleHtmlMimeTypes(baseType, mimeType);
 674 
 675                     for (String mt : mimeTypes) {
 676 
 677                         DataFlavor df = null;
 678 
 679                         try {
 680                             df = new DataFlavor(mt);
 681                             // Check for equality to plainTextFlavor so
 682                             // that we can ensure that the exact charset of
 683                             // plainTextFlavor, not the canonical charset
 684                             // or another equivalent charset with a
 685                             // different name, is used.
 686                             if (df.equals(DataFlavor.plainTextFlavor)) {
 687                                 df = DataFlavor.plainTextFlavor;
 688                             }
 689                         } catch (ClassNotFoundException cannotHappen) {
 690                         }
 691 
 692                         returnValue.add(df);
 693                     }
 694                 }
 695             }
 696 
 697             if (TEXT_PLAIN_BASE_TYPE.equals(baseType))
 698             {
 699                 returnValue.add(DataFlavor.plainTextFlavor);
 700             }
 701         } else {
 702             // Non-charset text natives should be treated as
 703             // opaque, 8-bit data in any of its various
 704             // representations.
 705             for (String encodedTextClassName : ENCODED_TEXT_CLASSES) {
 706                 DataFlavor toAdd = null;
 707                 try {
 708                     toAdd = new DataFlavor(baseType +
 709                          ";class=" + encodedTextClassName);
 710                 } catch (ClassNotFoundException cannotHappen) {
 711                 }
 712                 returnValue.add(toAdd);
 713             }
 714         }
 715         return returnValue;
 716     }
 717 
 718     private static final String [] htmlDocumentTypes =
 719             new String [] {"all", "selection", "fragment"};
 720 
 721     private static LinkedHashSet<String> handleHtmlMimeTypes(String baseType,
 722                                                              String mimeType) {
 723 
 724         LinkedHashSet<String> returnValues = new LinkedHashSet<>();
 725 
 726         if (HTML_TEXT_BASE_TYPE.equals(baseType)) {
 727             for (String documentType : htmlDocumentTypes) {
 728                 returnValues.add(mimeType + ";document=" + documentType);
 729             }
 730         } else {
 731             returnValues.add(mimeType);
 732         }
 733 
 734         return returnValues;
 735     }
 736 
 737     /**
 738      * Returns a <code>Map</code> of the specified <code>DataFlavor</code>s to
 739      * their most preferred <code>String</code> native. Each native value will
 740      * be the same as the first native in the List returned by
 741      * <code>getNativesForFlavor</code> for the specified flavor.
 742      * <p>
 743      * If a specified <code>DataFlavor</code> is previously unknown to the
 744      * data transfer subsystem, then invoking this method will establish a
 745      * mapping in both directions between the specified <code>DataFlavor</code>
 746      * and an encoded version of its MIME type as its native.
 747      *
 748      * @param flavors an array of <code>DataFlavor</code>s which will be the
 749      *        key set of the returned <code>Map</code>. If <code>null</code> is
 750      *        specified, a mapping of all <code>DataFlavor</code>s known to the
 751      *        data transfer subsystem to their most preferred
 752      *        <code>String</code> natives will be returned.
 753      * @return a <code>java.util.Map</code> of <code>DataFlavor</code>s to
 754      *         <code>String</code> natives
 755      *
 756      * @see #getNativesForFlavor
 757      * @see #encodeDataFlavor
 758      */
 759     @Override
 760     public synchronized Map<DataFlavor,String> getNativesForFlavors(DataFlavor[] flavors)
 761     {
 762         // Use getNativesForFlavor to generate extra natives for text flavors
 763         // and stringFlavor
 764 
 765         if (flavors == null) {
 766             List<DataFlavor> flavor_list = getFlavorsForNative(null);
 767             flavors = new DataFlavor[flavor_list.size()];
 768             flavor_list.toArray(flavors);
 769         }
 770 
 771         Map<DataFlavor, String> retval = new HashMap<>(flavors.length, 1.0f);
 772         for (DataFlavor flavor : flavors) {
 773             List<String> natives = getNativesForFlavor(flavor);
 774             String nat = (natives.isEmpty()) ? null : natives.get(0);
 775             retval.put(flavor, nat);
 776         }
 777 
 778         return retval;
 779     }
 780 
 781     /**
 782      * Returns a <code>Map</code> of the specified <code>String</code> natives
 783      * to their most preferred <code>DataFlavor</code>. Each
 784      * <code>DataFlavor</code> value will be the same as the first
 785      * <code>DataFlavor</code> in the List returned by
 786      * <code>getFlavorsForNative</code> for the specified native.
 787      * <p>
 788      * If a specified native is previously unknown to the data transfer
 789      * subsystem, and that native has been properly encoded, then invoking this
 790      * method will establish a mapping in both directions between the specified
 791      * native and a <code>DataFlavor</code> whose MIME type is a decoded
 792      * version of the native.
 793      *
 794      * @param natives an array of <code>String</code>s which will be the
 795      *        key set of the returned <code>Map</code>. If <code>null</code> is
 796      *        specified, a mapping of all supported <code>String</code> natives
 797      *        to their most preferred <code>DataFlavor</code>s will be
 798      *        returned.
 799      * @return a <code>java.util.Map</code> of <code>String</code> natives to
 800      *         <code>DataFlavor</code>s
 801      *
 802      * @see #getFlavorsForNative
 803      * @see #encodeJavaMIMEType
 804      */
 805     @Override
 806     public synchronized Map<String,DataFlavor> getFlavorsForNatives(String[] natives)
 807     {
 808         // Use getFlavorsForNative to generate extra flavors for text natives
 809         if (natives == null) {
 810             List<String> nativesList = getNativesForFlavor(null);
 811             natives = new String[nativesList.size()];
 812             nativesList.toArray(natives);
 813         }
 814 
 815         Map<String, DataFlavor> retval = new HashMap<>(natives.length, 1.0f);
 816         for (String aNative : natives) {
 817             List<DataFlavor> flavors = getFlavorsForNative(aNative);
 818             DataFlavor flav = (flavors.isEmpty())? null : flavors.get(0);
 819             retval.put(aNative, flav);
 820         }
 821         return retval;
 822     }
 823 
 824     /**
 825      * Adds a mapping from the specified <code>DataFlavor</code> (and all
 826      * <code>DataFlavor</code>s equal to the specified <code>DataFlavor</code>)
 827      * to the specified <code>String</code> native.
 828      * Unlike <code>getNativesForFlavor</code>, the mapping will only be
 829      * established in one direction, and the native will not be encoded. To
 830      * establish a two-way mapping, call
 831      * <code>addFlavorForUnencodedNative</code> as well. The new mapping will
 832      * be of lower priority than any existing mapping.
 833      * This method has no effect if a mapping from the specified or equal
 834      * <code>DataFlavor</code> to the specified <code>String</code> native
 835      * already exists.
 836      *
 837      * @param flav the <code>DataFlavor</code> key for the mapping
 838      * @param nat the <code>String</code> native value for the mapping
 839      * @throws NullPointerException if flav or nat is <code>null</code>
 840      *
 841      * @see #addFlavorForUnencodedNative
 842      * @since 1.4
 843      */
 844     public synchronized void addUnencodedNativeForFlavor(DataFlavor flav,
 845                                                          String nat) {
 846         Objects.requireNonNull(nat, "Null native not permitted");
 847         Objects.requireNonNull(flav, "Null flavor not permitted");
 848 
 849         LinkedHashSet<String> natives = getFlavorToNative().get(flav);
 850         if (natives == null) {
 851             natives = new LinkedHashSet<>(1);
 852             getFlavorToNative().put(flav, natives);
 853         }
 854         natives.add(nat);
 855         nativesForFlavorCache.remove(flav);
 856     }
 857 
 858     /**
 859      * Discards the current mappings for the specified <code>DataFlavor</code>
 860      * and all <code>DataFlavor</code>s equal to the specified
 861      * <code>DataFlavor</code>, and creates new mappings to the
 862      * specified <code>String</code> natives.
 863      * Unlike <code>getNativesForFlavor</code>, the mappings will only be
 864      * established in one direction, and the natives will not be encoded. To
 865      * establish two-way mappings, call <code>setFlavorsForNative</code>
 866      * as well. The first native in the array will represent the highest
 867      * priority mapping. Subsequent natives will represent mappings of
 868      * decreasing priority.
 869      * <p>
 870      * If the array contains several elements that reference equal
 871      * <code>String</code> natives, this method will establish new mappings
 872      * for the first of those elements and ignore the rest of them.
 873      * <p>
 874      * It is recommended that client code not reset mappings established by the
 875      * data transfer subsystem. This method should only be used for
 876      * application-level mappings.
 877      *
 878      * @param flav the <code>DataFlavor</code> key for the mappings
 879      * @param natives the <code>String</code> native values for the mappings
 880      * @throws NullPointerException if flav or natives is <code>null</code>
 881      *         or if natives contains <code>null</code> elements
 882      *
 883      * @see #setFlavorsForNative
 884      * @since 1.4
 885      */
 886     public synchronized void setNativesForFlavor(DataFlavor flav,
 887                                                  String[] natives) {
 888         Objects.requireNonNull(natives, "Null natives not permitted");
 889         Objects.requireNonNull(flav, "Null flavors not permitted");
 890 
 891         getFlavorToNative().remove(flav);
 892         for (String aNative : natives) {
 893             addUnencodedNativeForFlavor(flav, aNative);
 894         }
 895         disabledMappingGenerationKeys.add(flav);
 896         nativesForFlavorCache.remove(flav);
 897     }
 898 
 899     /**
 900      * Adds a mapping from a single <code>String</code> native to a single
 901      * <code>DataFlavor</code>. Unlike <code>getFlavorsForNative</code>, the
 902      * mapping will only be established in one direction, and the native will
 903      * not be encoded. To establish a two-way mapping, call
 904      * <code>addUnencodedNativeForFlavor</code> as well. The new mapping will
 905      * be of lower priority than any existing mapping.
 906      * This method has no effect if a mapping from the specified
 907      * <code>String</code> native to the specified or equal
 908      * <code>DataFlavor</code> already exists.
 909      *
 910      * @param nat the <code>String</code> native key for the mapping
 911      * @param flav the <code>DataFlavor</code> value for the mapping
 912      * @throws NullPointerException if nat or flav is <code>null</code>
 913      *
 914      * @see #addUnencodedNativeForFlavor
 915      * @since 1.4
 916      */
 917     public synchronized void addFlavorForUnencodedNative(String nat,
 918                                                          DataFlavor flav) {
 919         Objects.requireNonNull(nat, "Null native not permitted");
 920         Objects.requireNonNull(flav, "Null flavor not permitted");
 921 
 922         LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(nat);
 923         if (flavors == null) {
 924             flavors = new LinkedHashSet<>(1);
 925             getNativeToFlavor().put(nat, flavors);
 926         }
 927         flavors.add(flav);
 928         flavorsForNativeCache.remove(nat);
 929     }
 930 
 931     /**
 932      * Discards the current mappings for the specified <code>String</code>
 933      * native, and creates new mappings to the specified
 934      * <code>DataFlavor</code>s. Unlike <code>getFlavorsForNative</code>, the
 935      * mappings will only be established in one direction, and the natives need
 936      * not be encoded. To establish two-way mappings, call
 937      * <code>setNativesForFlavor</code> as well. The first
 938      * <code>DataFlavor</code> in the array will represent the highest priority
 939      * mapping. Subsequent <code>DataFlavor</code>s will represent mappings of
 940      * decreasing priority.
 941      * <p>
 942      * If the array contains several elements that reference equal
 943      * <code>DataFlavor</code>s, this method will establish new mappings
 944      * for the first of those elements and ignore the rest of them.
 945      * <p>
 946      * It is recommended that client code not reset mappings established by the
 947      * data transfer subsystem. This method should only be used for
 948      * application-level mappings.
 949      *
 950      * @param nat the <code>String</code> native key for the mappings
 951      * @param flavors the <code>DataFlavor</code> values for the mappings
 952      * @throws NullPointerException if nat or flavors is <code>null</code>
 953      *         or if flavors contains <code>null</code> elements
 954      *
 955      * @see #setNativesForFlavor
 956      * @since 1.4
 957      */
 958     public synchronized void setFlavorsForNative(String nat,
 959                                                  DataFlavor[] flavors) {
 960         Objects.requireNonNull(nat, "Null native not permitted");
 961         Objects.requireNonNull(flavors, "Null flavors not permitted");
 962 
 963         getNativeToFlavor().remove(nat);
 964         for (DataFlavor flavor : flavors) {
 965             addFlavorForUnencodedNative(nat, flavor);
 966         }
 967         disabledMappingGenerationKeys.add(nat);
 968         flavorsForNativeCache.remove(nat);
 969     }
 970 
 971     /**
 972      * Encodes a MIME type for use as a <code>String</code> native. The format
 973      * of an encoded representation of a MIME type is implementation-dependent.
 974      * The only restrictions are:
 975      * <ul>
 976      * <li>The encoded representation is <code>null</code> if and only if the
 977      * MIME type <code>String</code> is <code>null</code>.</li>
 978      * <li>The encoded representations for two non-<code>null</code> MIME type
 979      * <code>String</code>s are equal if and only if these <code>String</code>s
 980      * are equal according to <code>String.equals(Object)</code>.</li>
 981      * </ul>
 982      * <p>
 983      * The reference implementation of this method returns the specified MIME
 984      * type <code>String</code> prefixed with <code>JAVA_DATAFLAVOR:</code>.
 985      *
 986      * @param mimeType the MIME type to encode
 987      * @return the encoded <code>String</code>, or <code>null</code> if
 988      *         mimeType is <code>null</code>
 989      */
 990     public static String encodeJavaMIMEType(String mimeType) {
 991         return (mimeType != null)
 992             ? JavaMIME + mimeType
 993             : null;
 994     }
 995 
 996     /**
 997      * Encodes a <code>DataFlavor</code> for use as a <code>String</code>
 998      * native. The format of an encoded <code>DataFlavor</code> is
 999      * implementation-dependent. The only restrictions are:
1000      * <ul>
1001      * <li>The encoded representation is <code>null</code> if and only if the
1002      * specified <code>DataFlavor</code> is <code>null</code> or its MIME type
1003      * <code>String</code> is <code>null</code>.</li>
1004      * <li>The encoded representations for two non-<code>null</code>
1005      * <code>DataFlavor</code>s with non-<code>null</code> MIME type
1006      * <code>String</code>s are equal if and only if the MIME type
1007      * <code>String</code>s of these <code>DataFlavor</code>s are equal
1008      * according to <code>String.equals(Object)</code>.</li>
1009      * </ul>
1010      * <p>
1011      * The reference implementation of this method returns the MIME type
1012      * <code>String</code> of the specified <code>DataFlavor</code> prefixed
1013      * with <code>JAVA_DATAFLAVOR:</code>.
1014      *
1015      * @param flav the <code>DataFlavor</code> to encode
1016      * @return the encoded <code>String</code>, or <code>null</code> if
1017      *         flav is <code>null</code> or has a <code>null</code> MIME type
1018      */
1019     public static String encodeDataFlavor(DataFlavor flav) {
1020         return (flav != null)
1021             ? SystemFlavorMap.encodeJavaMIMEType(flav.getMimeType())
1022             : null;
1023     }
1024 
1025     /**
1026      * Returns whether the specified <code>String</code> is an encoded Java
1027      * MIME type.
1028      *
1029      * @param str the <code>String</code> to test
1030      * @return <code>true</code> if the <code>String</code> is encoded;
1031      *         <code>false</code> otherwise
1032      */
1033     public static boolean isJavaMIMEType(String str) {
1034         return (str != null && str.startsWith(JavaMIME, 0));
1035     }
1036 
1037     /**
1038      * Decodes a <code>String</code> native for use as a Java MIME type.
1039      *
1040      * @param nat the <code>String</code> to decode
1041      * @return the decoded Java MIME type, or <code>null</code> if nat is not
1042      *         an encoded <code>String</code> native
1043      */
1044     public static String decodeJavaMIMEType(String nat) {
1045         return (isJavaMIMEType(nat))
1046             ? nat.substring(JavaMIME.length(), nat.length()).trim()
1047             : null;
1048     }
1049 
1050     /**
1051      * Decodes a <code>String</code> native for use as a
1052      * <code>DataFlavor</code>.
1053      *
1054      * @param nat the <code>String</code> to decode
1055      * @return the decoded <code>DataFlavor</code>, or <code>null</code> if
1056      *         nat is not an encoded <code>String</code> native
1057      * @throws ClassNotFoundException if the class of the data flavor
1058      * is not loaded
1059      */
1060     public static DataFlavor decodeDataFlavor(String nat)
1061         throws ClassNotFoundException
1062     {
1063         String retval_str = SystemFlavorMap.decodeJavaMIMEType(nat);
1064         return (retval_str != null)
1065             ? new DataFlavor(retval_str)
1066             : null;
1067     }
1068 
1069     private static final class SoftCache<K, V> {
1070         Map<K, SoftReference<LinkedHashSet<V>>> cache;
1071 
1072         public void put(K key, LinkedHashSet<V> value) {
1073             if (cache == null) {
1074                 cache = new HashMap<>(1);
1075             }
1076             cache.put(key, new SoftReference<>(value));
1077         }
1078 
1079         public void remove(K key) {
1080             if (cache == null) return;
1081             cache.remove(null);
1082             cache.remove(key);
1083         }
1084 
1085         public LinkedHashSet<V> check(K key) {
1086             if (cache == null) return null;
1087             SoftReference<LinkedHashSet<V>> ref = cache.get(key);
1088             if (ref != null) {
1089                 return ref.get();
1090             }
1091             return null;
1092         }
1093     }
1094 }