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