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