/* |
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. |
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
* |
|
* This code is free software; you can redistribute it and/or modify it |
|
* under the terms of the GNU General Public License version 2 only, as |
|
* published by the Free Software Foundation. Oracle designates this |
|
* particular file as subject to the "Classpath" exception as provided |
|
* by Oracle in the LICENSE file that accompanied this code. |
|
* |
|
* This code is distributed in the hope that it will be useful, but WITHOUT |
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
* version 2 for more details (a copy is included in the LICENSE file that |
|
* accompanied this code). |
|
* |
|
* You should have received a copy of the GNU General Public License version |
|
* 2 along with this work; if not, write to the Free Software Foundation, |
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
* |
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
* or visit www.oracle.com if you need additional information or have any |
|
* questions. |
|
*/ |
|
package java.awt.datatransfer; |
|
import java.awt.Toolkit; |
|
import java.lang.ref.SoftReference; |
|
import java.io.BufferedReader; |
|
import java.io.File; |
|
import java.io.InputStreamReader; |
|
import java.io.IOException; |
|
import java.net.URL; |
|
import java.net.MalformedURLException; |
|
import java.util.ArrayList; |
|
import java.util.Collections; |
|
import java.util.HashMap; |
|
import java.util.HashSet; |
|
import java.util.LinkedHashSet; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.Objects; |
|
import java.util.Set; |
|
import sun.awt.AppContext; |
|
import sun.awt.datatransfer.DataTransferer; |
|
/** |
|
* The SystemFlavorMap is a configurable map between "natives" (Strings), which |
|
* correspond to platform-specific data formats, and "flavors" (DataFlavors), |
|
* which correspond to platform-independent MIME types. This mapping is used |
|
* by the data transfer subsystem to transfer data between Java and native |
|
* applications, and between Java applications in separate VMs. |
|
* <p> |
|
* |
|
* @since 1.2 |
|
*/ |
|
public final class SystemFlavorMap implements FlavorMap, FlavorTable { |
|
/** |
|
* Constant prefix used to tag Java types converted to native platform |
|
* type. |
|
*/ |
|
private static String JavaMIME = "JAVA_DATAFLAVOR:"; |
|
private static final Object FLAVOR_MAP_KEY = new Object(); |
|
/** |
|
* Copied from java.util.Properties. |
|
*/ |
|
private static final String keyValueSeparators = "=: \t\r\n\f"; |
|
private static final String strictKeyValueSeparators = "=:"; |
|
private static final String whiteSpaceChars = " \t\r\n\f"; |
|
/** |
|
* The list of valid, decoded text flavor representation classes, in order |
|
* from best to worst. |
|
*/ |
|
private static final String[] UNICODE_TEXT_CLASSES = { |
|
"java.io.Reader", "java.lang.String", "java.nio.CharBuffer", "\"[C\"" |
|
}; |
|
/** |
|
* The list of valid, encoded text flavor representation classes, in order |
|
* from best to worst. |
|
*/ |
|
private static final String[] ENCODED_TEXT_CLASSES = { |
|
"java.io.InputStream", "java.nio.ByteBuffer", "\"[B\"" |
|
}; |
|
/** |
|
* A String representing text/plain MIME type. |
|
*/ |
|
private static final String TEXT_PLAIN_BASE_TYPE = "text/plain"; |
|
/** |
|
* A String representing text/html MIME type. |
|
*/ |
|
private static final String HTML_TEXT_BASE_TYPE = "text/html"; |
|
/** |
|
* Maps native Strings to Lists of DataFlavors (or base type Strings for |
|
* text DataFlavors). |
|
* Do not use the field directly, use getNativeToFlavor() instead. |
|
*/ |
|
private final Map<String, LinkedHashSet<DataFlavor>> nativeToFlavor = new HashMap<>(); |
|
/** |
|
* Accessor to nativeToFlavor map. Since we use lazy initialization we must |
|
* use this accessor instead of direct access to the field which may not be |
|
* initialized yet. This method will initialize the field if needed. |
|
* |
|
* @return nativeToFlavor |
|
*/ |
|
private Map<String, LinkedHashSet<DataFlavor>> getNativeToFlavor() { |
|
if (!isMapInitialized) { |
|
initSystemFlavorMap(); |
|
} |
|
return nativeToFlavor; |
|
} |
|
/** |
|
* Maps DataFlavors (or base type Strings for text DataFlavors) to Lists of |
|
* native Strings. |
|
* Do not use the field directly, use getFlavorToNative() instead. |
|
*/ |
|
private final Map<DataFlavor, LinkedHashSet<String>> flavorToNative = new HashMap<>(); |
|
/** |
|
* Accessor to flavorToNative map. Since we use lazy initialization we must |
|
* use this accessor instead of direct access to the field which may not be |
|
* initialized yet. This method will initialize the field if needed. |
|
* |
|
* @return flavorToNative |
|
*/ |
|
private synchronized Map<DataFlavor, LinkedHashSet<String>> getFlavorToNative() { |
|
if (!isMapInitialized) { |
|
initSystemFlavorMap(); |
|
} |
|
return flavorToNative; |
|
} |
|
/** |
|
* Maps a text DataFlavor primary mime-type to the native. Used only to store |
|
* standard mappings registered in the flavormap.properties |
|
* Do not use this field directly, use getTextTypeToNative() instead. |
|
*/ |
|
private Map<String, LinkedHashSet<String>> textTypeToNative = new HashMap<>(); |
|
/** |
|
* Shows if the object has been initialized. |
|
*/ |
|
private boolean isMapInitialized = false; |
|
/** |
|
* An accessor to textTypeToNative map. Since we use lazy initialization we |
|
* must use this accessor instead of direct access to the field which may not |
|
* be initialized yet. This method will initialize the field if needed. |
|
* |
|
* @return textTypeToNative |
|
*/ |
|
private synchronized Map<String, LinkedHashSet<String>> getTextTypeToNative() { |
|
if (!isMapInitialized) { |
|
initSystemFlavorMap(); |
|
// From this point the map should not be modified |
|
textTypeToNative = Collections.unmodifiableMap(textTypeToNative); |
|
} |
|
return textTypeToNative; |
|
} |
|
/** |
|
* Caches the result of getNativesForFlavor(). Maps DataFlavors to |
|
* SoftReferences which reference LinkedHashSet of String natives. |
|
*/ |
|
private final SoftCache<DataFlavor, String> nativesForFlavorCache = new SoftCache<>(); |
|
/** |
|
* Caches the result getFlavorsForNative(). Maps String natives to |
|
* SoftReferences which reference LinkedHashSet of DataFlavors. |
|
*/ |
|
private final SoftCache<String, DataFlavor> flavorsForNativeCache = new SoftCache<>(); |
|
/** |
|
* Dynamic mapping generation used for text mappings should not be applied |
|
* to the DataFlavors and String natives for which the mappings have been |
|
* explicitly specified with setFlavorsForNative() or |
|
* setNativesForFlavor(). This keeps all such keys. |
|
*/ |
|
private Set<Object> disabledMappingGenerationKeys = new HashSet<>(); |
|
/** |
|
* Returns the default FlavorMap for this thread's ClassLoader. |
|
*/ |
|
public static FlavorMap getDefaultFlavorMap() { |
|
AppContext context = AppContext.getAppContext(); |
|
FlavorMap fm = (FlavorMap) context.get(FLAVOR_MAP_KEY); |
|
if (fm == null) { |
|
fm = new SystemFlavorMap(); |
|
context.put(FLAVOR_MAP_KEY, fm); |
|
} |
|
return fm; |
|
} |
|
private SystemFlavorMap() { |
|
} |
|
/** |
|
* Initializes a SystemFlavorMap by reading flavormap.properties and |
|
* AWT.DnD.flavorMapFileURL. |
|
* For thread-safety must be called under lock on this. |
|
*/ |
|
private void initSystemFlavorMap() { |
|
if (isMapInitialized) { |
|
return; |
|
} |
|
isMapInitialized = true; |
|
BufferedReader flavormapDotProperties = |
|
java.security.AccessController.doPrivileged( |
|
new java.security.PrivilegedAction<BufferedReader>() { |
|
public BufferedReader run() { |
|
String fileName = |
|
System.getProperty("java.home") + |
|
File.separator + |
|
"lib" + |
|
File.separator + |
|
"flavormap.properties"; |
|
try { |
|
return new BufferedReader |
|
(new InputStreamReader |
|
(new File(fileName).toURI().toURL().openStream(), "ISO-8859-1")); |
|
} catch (MalformedURLException e) { |
|
System.err.println("MalformedURLException:" + e + " while loading default flavormap.properties file:" + fileName); |
|
} catch (IOException e) { |
|
System.err.println("IOException:" + e + " while loading default flavormap.properties file:" + fileName); |
|
} |
|
return null; |
|
} |
|
}); |
|
String url = |
|
java.security.AccessController.doPrivileged( |
|
new java.security.PrivilegedAction<String>() { |
|
public String run() { |
|
return Toolkit.getProperty("AWT.DnD.flavorMapFileURL", null); |
|
} |
|
}); |
|
if (flavormapDotProperties != null) { |
|
try { |
|
parseAndStoreReader(flavormapDotProperties); |
|
} catch (IOException e) { |
|
System.err.println("IOException:" + e + " while parsing default flavormap.properties file"); |
|
} |
|
} |
|
BufferedReader flavormapURL = null; |
|
if (url != null) { |
|
try { |
|
flavormapURL = new BufferedReader(new InputStreamReader(new URL(url).openStream(), "ISO-8859-1")); |
|
} catch (MalformedURLException e) { |
|
System.err.println("MalformedURLException:" + e + " while reading AWT.DnD.flavorMapFileURL:" + url); |
|
} catch (IOException e) { |
|
System.err.println("IOException:" + e + " while reading AWT.DnD.flavorMapFileURL:" + url); |
|
} catch (SecurityException e) { |
|
// ignored |
|
} |
|
} |
|
if (flavormapURL != null) { |
|
try { |
|
parseAndStoreReader(flavormapURL); |
|
} catch (IOException e) { |
|
System.err.println("IOException:" + e + " while parsing AWT.DnD.flavorMapFileURL"); |
|
} |
|
} |
|
} |
|
/** |
|
* Copied code from java.util.Properties. Parsing the data ourselves is the |
|
* only way to handle duplicate keys and values. |
|
*/ |
|
private void parseAndStoreReader(BufferedReader in) throws IOException { |
|
while (true) { |
|
// Get next line |
|
String line = in.readLine(); |
|
if (line == null) { |
|
return; |
|
} |
|
if (line.length() > 0) { |
|
// Continue lines that end in slashes if they are not comments |
|
char firstChar = line.charAt(0); |
|
if (firstChar != '#' && firstChar != '!') { |
|
while (continueLine(line)) { |
|
String nextLine = in.readLine(); |
|
if (nextLine == null) { |
|
nextLine = ""; |
|
} |
|
String loppedLine = |
|
line.substring(0, line.length() - 1); |
|
// Advance beyond whitespace on new line |
|
int startIndex = 0; |
|
for(; startIndex < nextLine.length(); startIndex++) { |
|
if (whiteSpaceChars. |
|
indexOf(nextLine.charAt(startIndex)) == -1) |
|
{ |
|
break; |
|
} |
|
} |
|
nextLine = nextLine.substring(startIndex, |
|
nextLine.length()); |
|
line = loppedLine+nextLine; |
|
} |
|
// Find start of key |
|
int len = line.length(); |
|
int keyStart = 0; |
|
for(; keyStart < len; keyStart++) { |
|
if(whiteSpaceChars. |
|
indexOf(line.charAt(keyStart)) == -1) { |
|
break; |
|
} |
|
} |
|
// Blank lines are ignored |
|
if (keyStart == len) { |
|
continue; |
|
} |
|
// Find separation between key and value |
|
int separatorIndex = keyStart; |
|
for(; separatorIndex < len; separatorIndex++) { |
|
char currentChar = line.charAt(separatorIndex); |
|
if (currentChar == '\\') { |
|
separatorIndex++; |
|
} else if (keyValueSeparators. |
|
indexOf(currentChar) != -1) { |
|
break; |
|
} |
|
} |
|
// Skip over whitespace after key if any |
|
int valueIndex = separatorIndex; |
|
for (; valueIndex < len; valueIndex++) { |
|
if (whiteSpaceChars. |
|
indexOf(line.charAt(valueIndex)) == -1) { |
|
break; |
|
} |
|
} |
|
// Skip over one non whitespace key value separators if any |
|
if (valueIndex < len) { |
|
if (strictKeyValueSeparators. |
|
indexOf(line.charAt(valueIndex)) != -1) { |
|
valueIndex++; |
|
} |
|
} |
|
// Skip over white space after other separators if any |
|
while (valueIndex < len) { |
|
if (whiteSpaceChars. |
|
indexOf(line.charAt(valueIndex)) == -1) { |
|
break; |
|
} |
|
valueIndex++; |
|
} |
|
String key = line.substring(keyStart, separatorIndex); |
|
String value = (separatorIndex < len) |
|
? line.substring(valueIndex, len) |
|
: ""; |
|
// Convert then store key and value |
|
key = loadConvert(key); |
|
value = loadConvert(value); |
|
try { |
|
MimeType mime = new MimeType(value); |
|
if ("text".equals(mime.getPrimaryType())) { |
|
String charset = mime.getParameter("charset"); |
|
if (DataTransferer.doesSubtypeSupportCharset |
|
(mime.getSubType(), charset)) |
|
{ |
|
// We need to store the charset and eoln |
|
// parameters, if any, so that the |
|
// DataTransferer will have this information |
|
// for conversion into the native format. |
|
DataTransferer transferer = |
|
DataTransferer.getInstance(); |
|
if (transferer != null) { |
|
transferer.registerTextFlavorProperties |
|
(key, charset, |
|
mime.getParameter("eoln"), |
|
mime.getParameter("terminators")); |
|
} |
|
} |
|
// But don't store any of these parameters in the |
|
// DataFlavor itself for any text natives (even |
|
// non-charset ones). The SystemFlavorMap will |
|
// synthesize the appropriate mappings later. |
|
mime.removeParameter("charset"); |
|
mime.removeParameter("class"); |
|
mime.removeParameter("eoln"); |
|
mime.removeParameter("terminators"); |
|
value = mime.toString(); |
|
} |
|
} catch (MimeTypeParseException e) { |
|
e.printStackTrace(); |
|
continue; |
|
} |
|
DataFlavor flavor; |
|
try { |
|
flavor = new DataFlavor(value); |
|
} catch (Exception e) { |
|
try { |
|
flavor = new DataFlavor(value, null); |
|
} catch (Exception ee) { |
|
ee.printStackTrace(); |
|
continue; |
|
} |
|
} |
|
final LinkedHashSet<DataFlavor> dfs = new LinkedHashSet<>(); |
|
dfs.add(flavor); |
|
if ("text".equals(flavor.getPrimaryType())) { |
|
dfs.addAll(convertMimeTypeToDataFlavors(value)); |
|
store(flavor.mimeType.getBaseType(), key, getTextTypeToNative()); |
|
} |
|
for (DataFlavor df : dfs) { |
|
store(df, key, getFlavorToNative()); |
|
store(key, df, getNativeToFlavor()); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
/** |
|
* Copied from java.util.Properties. |
|
*/ |
|
private boolean continueLine (String line) { |
|
int slashCount = 0; |
|
int index = line.length() - 1; |
|
while((index >= 0) && (line.charAt(index--) == '\\')) { |
|
slashCount++; |
|
} |
|
return (slashCount % 2 == 1); |
|
} |
|
/** |
|
* Copied from java.util.Properties. |
|
*/ |
|
private String loadConvert(String theString) { |
|
char aChar; |
|
int len = theString.length(); |
|
StringBuilder outBuffer = new StringBuilder(len); |
|
for (int x = 0; x < len; ) { |
|
aChar = theString.charAt(x++); |
|
if (aChar == '\\') { |
|
aChar = theString.charAt(x++); |
|
if (aChar == 'u') { |
|
// Read the xxxx |
|
int value = 0; |
|
for (int i = 0; i < 4; i++) { |
|
aChar = theString.charAt(x++); |
|
switch (aChar) { |
|
case '0': case '1': case '2': case '3': case '4': |
|
case '5': case '6': case '7': case '8': case '9': { |
|
value = (value << 4) + aChar - '0'; |
|
break; |
|
} |
|
case 'a': case 'b': case 'c': |
|
case 'd': case 'e': case 'f': { |
|
value = (value << 4) + 10 + aChar - 'a'; |
|
break; |
|
} |
|
case 'A': case 'B': case 'C': |
|
case 'D': case 'E': case 'F': { |
|
value = (value << 4) + 10 + aChar - 'A'; |
|
break; |
|
} |
|
default: { |
|
throw new IllegalArgumentException( |
|
"Malformed \\uxxxx encoding."); |
|
} |
|
} |
|
} |
|
outBuffer.append((char)value); |
|
} else { |
|
if (aChar == 't') { |
|
aChar = '\t'; |
|
} else if (aChar == 'r') { |
|
aChar = '\r'; |
|
} else if (aChar == 'n') { |
|
aChar = '\n'; |
|
} else if (aChar == 'f') { |
|
aChar = '\f'; |
|
} |
|
outBuffer.append(aChar); |
|
} |
|
} else { |
|
outBuffer.append(aChar); |
|
} |
|
} |
|
return outBuffer.toString(); |
|
} |
|
/** |
|
* Stores the listed object under the specified hash key in map. Unlike a |
|
* standard map, the listed object will not replace any object already at |
|
* the appropriate Map location, but rather will be appended to a List |
|
* stored in that location. |
|
*/ |
|
private <H, L> void store(H hashed, L listed, Map<H, LinkedHashSet<L>> map) { |
|
LinkedHashSet<L> list = map.get(hashed); |
|
if (list == null) { |
|
list = new LinkedHashSet<>(1); |
|
map.put(hashed, list); |
|
} |
|
if (!list.contains(listed)) { |
|
list.add(listed); |
|
} |
|
} |
|
/** |
|
* Semantically equivalent to 'nativeToFlavor.get(nat)'. This method |
|
* handles the case where 'nat' is not found in 'nativeToFlavor'. In that |
|
* case, a new DataFlavor is synthesized, stored, and returned, if and |
|
* only if the specified native is encoded as a Java MIME type. |
|
*/ |
|
private LinkedHashSet<DataFlavor> nativeToFlavorLookup(String nat) { |
|
LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(nat); |
|
if (nat != null && !disabledMappingGenerationKeys.contains(nat)) { |
|
DataTransferer transferer = DataTransferer.getInstance(); |
|
if (transferer != null) { |
|
LinkedHashSet<DataFlavor> platformFlavors = |
|
transferer.getPlatformMappingsForNative(nat); |
|
if (!platformFlavors.isEmpty()) { |
|
if (flavors != null) { |
|
// Prepending the platform-specific mappings ensures |
|
// that the flavors added with |
|
// addFlavorForUnencodedNative() are at the end of |
|
// list. |
|
platformFlavors.addAll(flavors); |
|
} |
|
flavors = platformFlavors; |
|
} |
|
} |
|
} |
|
if (flavors == null && isJavaMIMEType(nat)) { |
|
String decoded = decodeJavaMIMEType(nat); |
|
DataFlavor flavor = null; |
|
try { |
|
flavor = new DataFlavor(decoded); |
|
} catch (Exception e) { |
|
System.err.println("Exception \"" + e.getClass().getName() + |
|
": " + e.getMessage() + |
|
"\"while constructing DataFlavor for: " + |
|
decoded); |
|
} |
|
if (flavor != null) { |
|
flavors = new LinkedHashSet<>(1); |
|
getNativeToFlavor().put(nat, flavors); |
|
flavors.add(flavor); |
|
flavorsForNativeCache.remove(nat); |
|
LinkedHashSet<String> natives = getFlavorToNative().get(flavor); |
|
if (natives == null) { |
|
natives = new LinkedHashSet<>(1); |
|
getFlavorToNative().put(flavor, natives); |
|
} |
|
natives.add(nat); |
|
nativesForFlavorCache.remove(flavor); |
|
} |
|
} |
|
return (flavors != null) ? flavors : new LinkedHashSet<>(0); |
|
} |
|
/** |
|
* Semantically equivalent to 'flavorToNative.get(flav)'. This method |
|
* handles the case where 'flav' is not found in 'flavorToNative' depending |
|
* on the value of passes 'synthesize' parameter. If 'synthesize' is |
|
* SYNTHESIZE_IF_NOT_FOUND a native is synthesized, stored, and returned by |
|
* encoding the DataFlavor's MIME type. Otherwise an empty List is returned |
|
* and 'flavorToNative' remains unaffected. |
|
*/ |
|
private LinkedHashSet<String> flavorToNativeLookup(final DataFlavor flav, |
|
final boolean synthesize) { |
|
LinkedHashSet<String> natives = getFlavorToNative().get(flav); |
|
if (flav != null && !disabledMappingGenerationKeys.contains(flav)) { |
|
DataTransferer transferer = DataTransferer.getInstance(); |
|
if (transferer != null) { |
|
LinkedHashSet<String> platformNatives = |
|
transferer.getPlatformMappingsForFlavor(flav); |
|
if (!platformNatives.isEmpty()) { |
|
if (natives != null) { |
|
// Prepend the platform-specific mappings to ensure |
|
// that the natives added with |
|
// addUnencodedNativeForFlavor() are at the end of |
|
// list. |
|
platformNatives.addAll(natives); |
|
} |
|
natives = platformNatives; |
|
} |
|
} |
|
} |
|
if (natives == null) { |
|
if (synthesize) { |
|
String encoded = encodeDataFlavor(flav); |
|
natives = new LinkedHashSet<>(1); |
|
getFlavorToNative().put(flav, natives); |
|
natives.add(encoded); |
|
LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(encoded); |
|
if (flavors == null) { |
|
flavors = new LinkedHashSet<>(1); |
|
getNativeToFlavor().put(encoded, flavors); |
|
} |
|
flavors.add(flav); |
|
nativesForFlavorCache.remove(flav); |
|
flavorsForNativeCache.remove(encoded); |
|
} else { |
|
natives = new LinkedHashSet<>(0); |
|
} |
|
} |
|
return new LinkedHashSet<>(natives); |
|
} |
|
/** |
|
* Returns a <code>List</code> of <code>String</code> natives to which the |
|
* specified <code>DataFlavor</code> can be translated by the data transfer |
|
* subsystem. The <code>List</code> will be sorted from best native to |
|
* worst. That is, the first native will best reflect data in the specified |
|
* flavor to the underlying native platform. |
|
* <p> |
|
* If the specified <code>DataFlavor</code> is previously unknown to the |
|
* data transfer subsystem and the data transfer subsystem is unable to |
|
* translate this <code>DataFlavor</code> to any existing native, then |
|
* invoking this method will establish a |
|
* mapping in both directions between the specified <code>DataFlavor</code> |
|
* and an encoded version of its MIME type as its native. |
|
* |
|
* @param flav the <code>DataFlavor</code> whose corresponding natives |
|
* should be returned. If <code>null</code> is specified, all |
|
* natives currently known to the data transfer subsystem are |
|
* returned in a non-deterministic order. |
|
* @return a <code>java.util.List</code> of <code>java.lang.String</code> |
|
* objects which are platform-specific representations of platform- |
|
* specific data formats |
|
* |
|
* @see #encodeDataFlavor |
|
* @since 1.4 |
|
*/ |
|
@Override |
|
public synchronized List<String> getNativesForFlavor(DataFlavor flav) { |
|
LinkedHashSet<String> retval = nativesForFlavorCache.check(flav); |
|
if (retval != null) { |
|
return new ArrayList<>(retval); |
|
} |
|
if (flav == null) { |
|
retval = new LinkedHashSet<>(getNativeToFlavor().keySet()); |
|
} else if (disabledMappingGenerationKeys.contains(flav)) { |
|
// In this case we shouldn't synthesize a native for this flavor, |
|
// since its mappings were explicitly specified. |
|
retval = flavorToNativeLookup(flav, false); |
|
} else if (DataTransferer.isFlavorCharsetTextType(flav)) { |
|
retval = new LinkedHashSet<>(0); |
|
// For text/* flavors, flavor-to-native mappings specified in |
|
// flavormap.properties are stored per flavor's base type. |
|
if ("text".equals(flav.getPrimaryType())) { |
|
LinkedHashSet<String> textTypeNatives = |
|
getTextTypeToNative().get(flav.mimeType.getBaseType()); |
|
if (textTypeNatives != null) { |
|
retval.addAll(textTypeNatives); |
|
} |
|
} |
|
// Also include text/plain natives, but don't duplicate Strings |
|
LinkedHashSet<String> textTypeNatives = |
|
getTextTypeToNative().get(TEXT_PLAIN_BASE_TYPE); |
|
if (textTypeNatives != null) { |
|
retval.addAll(textTypeNatives); |
|
} |
|
if (retval.isEmpty()) { |
|
retval = flavorToNativeLookup(flav, true); |
|
} else { |
|
// In this branch it is guaranteed that natives explicitly |
|
// listed for flav's MIME type were added with |
|
// addUnencodedNativeForFlavor(), so they have lower priority. |
|
retval.addAll(flavorToNativeLookup(flav, false)); |
|
} |
|
} else if (DataTransferer.isFlavorNoncharsetTextType(flav)) { |
|
retval = getTextTypeToNative().get(flav.mimeType.getBaseType()); |
|
if (retval == null || retval.isEmpty()) { |
|
retval = flavorToNativeLookup(flav, true); |
|
} else { |
|
// In this branch it is guaranteed that natives explicitly |
|
// listed for flav's MIME type were added with |
|
// addUnencodedNativeForFlavor(), so they have lower priority. |
|
retval.addAll(flavorToNativeLookup(flav, false)); |
|
} |
|
} else { |
|
retval = flavorToNativeLookup(flav, true); |
|
} |
|
nativesForFlavorCache.put(flav, retval); |
|
// Create a copy, because client code can modify the returned list. |
|
return new ArrayList<>(retval); |
|
} |
|
/** |
|
* Returns a <code>List</code> of <code>DataFlavor</code>s to which the |
|
* specified <code>String</code> native can be translated by the data |
|
* transfer subsystem. The <code>List</code> will be sorted from best |
|
* <code>DataFlavor</code> to worst. That is, the first |
|
* <code>DataFlavor</code> will best reflect data in the specified |
|
* native to a Java application. |
|
* <p> |
|
* If the specified native is previously unknown to the data transfer |
|
* subsystem, and that native has been properly encoded, then invoking this |
|
* method will establish a mapping in both directions between the specified |
|
* native and a <code>DataFlavor</code> whose MIME type is a decoded |
|
* version of the native. |
|
* <p> |
|
* If the specified native is not a properly encoded native and the |
|
* mappings for this native have not been altered with |
|
* <code>setFlavorsForNative</code>, then the contents of the |
|
* <code>List</code> is platform dependent, but <code>null</code> |
|
* cannot be returned. |
|
* |
|
* @param nat the native whose corresponding <code>DataFlavor</code>s |
|
* should be returned. If <code>null</code> is specified, all |
|
* <code>DataFlavor</code>s currently known to the data transfer |
|
* subsystem are returned in a non-deterministic order. |
|
* @return a <code>java.util.List</code> of <code>DataFlavor</code> |
|
* objects into which platform-specific data in the specified, |
|
* platform-specific native can be translated |
|
* |
|
* @see #encodeJavaMIMEType |
|
* @since 1.4 |
|
*/ |
|
@Override |
|
public synchronized List<DataFlavor> getFlavorsForNative(String nat) { |
|
LinkedHashSet<DataFlavor> returnValue = flavorsForNativeCache.check(nat); |
|
if (returnValue != null) { |
|
return new ArrayList<>(returnValue); |
|
} else { |
|
returnValue = new LinkedHashSet<>(); |
|
} |
|
if (nat == null) { |
|
for (String n : getNativesForFlavor(null)) { |
|
returnValue.addAll(getFlavorsForNative(n)); |
|
} |
|
} else { |
|
final LinkedHashSet<DataFlavor> flavors = nativeToFlavorLookup(nat); |
|
if (disabledMappingGenerationKeys.contains(nat)) { |
|
return new ArrayList<>(flavors); |
|
} |
|
final LinkedHashSet<DataFlavor> flavorsWithSynthesized = |
|
nativeToFlavorLookup(nat); |
|
for (DataFlavor df : flavorsWithSynthesized) { |
|
returnValue.add(df); |
|
if ("text".equals(df.getPrimaryType())) { |
|
String baseType = df.mimeType.getBaseType(); |
|
returnValue.addAll(convertMimeTypeToDataFlavors(baseType)); |
|
} |
|
} |
|
} |
|
flavorsForNativeCache.put(nat, returnValue); |
|
return new ArrayList<>(returnValue); |
|
} |
|
private static Set<DataFlavor> convertMimeTypeToDataFlavors( |
|
final String baseType) { |
|
final Set<DataFlavor> returnValue = new LinkedHashSet<>(); |
|
String subType = null; |
|
try { |
|
final MimeType mimeType = new MimeType(baseType); |
|
subType = mimeType.getSubType(); |
|
} catch (MimeTypeParseException mtpe) { |
|
// Cannot happen, since we checked all mappings |
|
// on load from flavormap.properties. |
|
} |
|
if (DataTransferer.doesSubtypeSupportCharset(subType, null)) { |
|
if (TEXT_PLAIN_BASE_TYPE.equals(baseType)) |
|
{ |
|
returnValue.add(DataFlavor.stringFlavor); |
|
} |
|
for (String unicodeClassName : UNICODE_TEXT_CLASSES) { |
|
final String mimeType = baseType + ";charset=Unicode;class=" + |
|
unicodeClassName; |
|
final LinkedHashSet<String> mimeTypes = |
|
handleHtmlMimeTypes(baseType, mimeType); |
|
for (String mt : mimeTypes) { |
|
DataFlavor toAdd = null; |
|
try { |
|
toAdd = new DataFlavor(mt); |
|
} catch (ClassNotFoundException cannotHappen) { |
|
} |
|
returnValue.add(toAdd); |
|
} |
|
} |
|
for (String charset : DataTransferer.standardEncodings()) { |
|
for (String encodedTextClass : ENCODED_TEXT_CLASSES) { |
|
final String mimeType = |
|
baseType + ";charset=" + charset + |
|
";class=" + encodedTextClass; |
|
final LinkedHashSet<String> mimeTypes = |
|
handleHtmlMimeTypes(baseType, mimeType); |
|
for (String mt : mimeTypes) { |
|
DataFlavor df = null; |
|
try { |
|
df = new DataFlavor(mt); |
|
// Check for equality to plainTextFlavor so |
|
// that we can ensure that the exact charset of |
|
// plainTextFlavor, not the canonical charset |
|
// or another equivalent charset with a |
|
// different name, is used. |
|
if (df.equals(DataFlavor.plainTextFlavor)) { |
|
df = DataFlavor.plainTextFlavor; |
|
} |
|
} catch (ClassNotFoundException cannotHappen) { |
|
} |
|
returnValue.add(df); |
|
} |
|
} |
|
} |
|
if (TEXT_PLAIN_BASE_TYPE.equals(baseType)) |
|
{ |
|
returnValue.add(DataFlavor.plainTextFlavor); |
|
} |
|
} else { |
|
// Non-charset text natives should be treated as |
|
// opaque, 8-bit data in any of its various |
|
// representations. |
|
for (String encodedTextClassName : ENCODED_TEXT_CLASSES) { |
|
DataFlavor toAdd = null; |
|
try { |
|
toAdd = new DataFlavor(baseType + |
|
";class=" + encodedTextClassName); |
|
} catch (ClassNotFoundException cannotHappen) { |
|
} |
|
returnValue.add(toAdd); |
|
} |
|
} |
|
return returnValue; |
|
} |
|
private static final String [] htmlDocumntTypes = |
|
new String [] {"all", "selection", "fragment"}; |
|
private static LinkedHashSet<String> handleHtmlMimeTypes(String baseType, |
|
String mimeType) { |
|
LinkedHashSet<String> returnValues = new LinkedHashSet<>(); |
|
if (HTML_TEXT_BASE_TYPE.equals(baseType)) { |
|
for (String documentType : htmlDocumntTypes) { |
|
returnValues.add(mimeType + ";document=" + documentType); |
|
} |
|
} else { |
|
returnValues.add(mimeType); |
|
} |
|
return returnValues; |
|
} |
|
/** |
|
* Returns a <code>Map</code> of the specified <code>DataFlavor</code>s to |
|
* their most preferred <code>String</code> native. Each native value will |
|
* be the same as the first native in the List returned by |
|
* <code>getNativesForFlavor</code> for the specified flavor. |
|
* <p> |
|
* If a specified <code>DataFlavor</code> is previously unknown to the |
|
* data transfer subsystem, then invoking this method will establish a |
|
* mapping in both directions between the specified <code>DataFlavor</code> |
|
* and an encoded version of its MIME type as its native. |
|
* |
|
* @param flavors an array of <code>DataFlavor</code>s which will be the |
|
* key set of the returned <code>Map</code>. If <code>null</code> is |
|
* specified, a mapping of all <code>DataFlavor</code>s known to the |
|
* data transfer subsystem to their most preferred |
|
* <code>String</code> natives will be returned. |
|
* @return a <code>java.util.Map</code> of <code>DataFlavor</code>s to |
|
* <code>String</code> natives |
|
* |
|
* @see #getNativesForFlavor |
|
* @see #encodeDataFlavor |
|
*/ |
|
@Override |
|
public synchronized Map<DataFlavor,String> getNativesForFlavors(DataFlavor[] flavors) |
|
{ |
|
// Use getNativesForFlavor to generate extra natives for text flavors |
|
// and stringFlavor |
|
if (flavors == null) { |
|
List<DataFlavor> flavor_list = getFlavorsForNative(null); |
|
flavors = new DataFlavor[flavor_list.size()]; |
|
flavor_list.toArray(flavors); |
|
} |
|
Map<DataFlavor, String> retval = new HashMap<>(flavors.length, 1.0f); |
|
for (DataFlavor flavor : flavors) { |
|
List<String> natives = getNativesForFlavor(flavor); |
|
String nat = (natives.isEmpty()) ? null : natives.get(0); |
|
retval.put(flavor, nat); |
|
} |
|
return retval; |
|
} |
|
/** |
|
* Returns a <code>Map</code> of the specified <code>String</code> natives |
|
* to their most preferred <code>DataFlavor</code>. Each |
|
* <code>DataFlavor</code> value will be the same as the first |
|
* <code>DataFlavor</code> in the List returned by |
|
* <code>getFlavorsForNative</code> for the specified native. |
|
* <p> |
|
* If a specified native is previously unknown to the data transfer |
|
* subsystem, and that native has been properly encoded, then invoking this |
|
* method will establish a mapping in both directions between the specified |
|
* native and a <code>DataFlavor</code> whose MIME type is a decoded |
|
* version of the native. |
|
* |
|
* @param natives an array of <code>String</code>s which will be the |
|
* key set of the returned <code>Map</code>. If <code>null</code> is |
|
* specified, a mapping of all supported <code>String</code> natives |
|
* to their most preferred <code>DataFlavor</code>s will be |
|
* returned. |
|
* @return a <code>java.util.Map</code> of <code>String</code> natives to |
|
* <code>DataFlavor</code>s |
|
* |
|
* @see #getFlavorsForNative |
|
* @see #encodeJavaMIMEType |
|
*/ |
|
@Override |
|
public synchronized Map<String,DataFlavor> getFlavorsForNatives(String[] natives) |
|
{ |
|
// Use getFlavorsForNative to generate extra flavors for text natives |
|
if (natives == null) { |
|
List<String> nativesList = getNativesForFlavor(null); |
|
natives = new String[nativesList.size()]; |
|
nativesList.toArray(natives); |
|
} |
|
Map<String, DataFlavor> retval = new HashMap<>(natives.length, 1.0f); |
|
for (String aNative : natives) { |
|
List<DataFlavor> flavors = getFlavorsForNative(aNative); |
|
DataFlavor flav = (flavors.isEmpty())? null : flavors.get(0); |
|
retval.put(aNative, flav); |
|
} |
|
return retval; |
|
} |
|
/** |
|
* Adds a mapping from the specified <code>DataFlavor</code> (and all |
|
* <code>DataFlavor</code>s equal to the specified <code>DataFlavor</code>) |
|
* to the specified <code>String</code> native. |
|
* Unlike <code>getNativesForFlavor</code>, the mapping will only be |
|
* established in one direction, and the native will not be encoded. To |
|
* establish a two-way mapping, call |
|
* <code>addFlavorForUnencodedNative</code> as well. The new mapping will |
|
* be of lower priority than any existing mapping. |
|
* This method has no effect if a mapping from the specified or equal |
|
* <code>DataFlavor</code> to the specified <code>String</code> native |
|
* already exists. |
|
* |
|
* @param flav the <code>DataFlavor</code> key for the mapping |
|
* @param nat the <code>String</code> native value for the mapping |
|
* @throws NullPointerException if flav or nat is <code>null</code> |
|
* |
|
* @see #addFlavorForUnencodedNative |
|
* @since 1.4 |
|
*/ |
|
public synchronized void addUnencodedNativeForFlavor(DataFlavor flav, |
|
String nat) { |
|
Objects.requireNonNull(nat, "Null native not permitted"); |
|
Objects.requireNonNull(flav, "Null flavor not permitted"); |
|
LinkedHashSet<String> natives = getFlavorToNative().get(flav); |
|
if (natives == null) { |
|
natives = new LinkedHashSet<>(1); |
|
getFlavorToNative().put(flav, natives); |
|
} |
|
natives.add(nat); |
|
nativesForFlavorCache.remove(flav); |
|
} |
|
/** |
|
* Discards the current mappings for the specified <code>DataFlavor</code> |
|
* and all <code>DataFlavor</code>s equal to the specified |
|
* <code>DataFlavor</code>, and creates new mappings to the |
|
* specified <code>String</code> natives. |
|
* Unlike <code>getNativesForFlavor</code>, the mappings will only be |
|
* established in one direction, and the natives will not be encoded. To |
|
* establish two-way mappings, call <code>setFlavorsForNative</code> |
|
* as well. The first native in the array will represent the highest |
|
* priority mapping. Subsequent natives will represent mappings of |
|
* decreasing priority. |
|
* <p> |
|
* If the array contains several elements that reference equal |
|
* <code>String</code> natives, this method will establish new mappings |
|
* for the first of those elements and ignore the rest of them. |
|
* <p> |
|
* It is recommended that client code not reset mappings established by the |
|
* data transfer subsystem. This method should only be used for |
|
* application-level mappings. |
|
* |
|
* @param flav the <code>DataFlavor</code> key for the mappings |
|
* @param natives the <code>String</code> native values for the mappings |
|
* @throws NullPointerException if flav or natives is <code>null</code> |
|
* or if natives contains <code>null</code> elements |
|
* |
|
* @see #setFlavorsForNative |
|
* @since 1.4 |
|
*/ |
|
public synchronized void setNativesForFlavor(DataFlavor flav, |
|
String[] natives) { |
|
Objects.requireNonNull(natives, "Null natives not permitted"); |
|
Objects.requireNonNull(flav, "Null flavors not permitted"); |
|
getFlavorToNative().remove(flav); |
|
for (String aNative : natives) { |
|
addUnencodedNativeForFlavor(flav, aNative); |
|
} |
|
disabledMappingGenerationKeys.add(flav); |
|
nativesForFlavorCache.remove(flav); |
|
} |
|
/** |
|
* Adds a mapping from a single <code>String</code> native to a single |
|
* <code>DataFlavor</code>. Unlike <code>getFlavorsForNative</code>, the |
|
* mapping will only be established in one direction, and the native will |
|
* not be encoded. To establish a two-way mapping, call |
|
* <code>addUnencodedNativeForFlavor</code> as well. The new mapping will |
|
* be of lower priority than any existing mapping. |
|
* This method has no effect if a mapping from the specified |
|
* <code>String</code> native to the specified or equal |
|
* <code>DataFlavor</code> already exists. |
|
* |
|
* @param nat the <code>String</code> native key for the mapping |
|
* @param flav the <code>DataFlavor</code> value for the mapping |
|
* @throws NullPointerException if nat or flav is <code>null</code> |
|
* |
|
* @see #addUnencodedNativeForFlavor |
|
* @since 1.4 |
|
*/ |
|
public synchronized void addFlavorForUnencodedNative(String nat, |
|
DataFlavor flav) { |
|
Objects.requireNonNull(nat, "Null native not permitted"); |
|
Objects.requireNonNull(flav, "Null flavor not permitted"); |
|
LinkedHashSet<DataFlavor> flavors = getNativeToFlavor().get(nat); |
|
if (flavors == null) { |
|
flavors = new LinkedHashSet<>(1); |
|
getNativeToFlavor().put(nat, flavors); |
|
} |
|
flavors.add(flav); |
|
flavorsForNativeCache.remove(nat); |
|
} |
|
/** |
|
* Discards the current mappings for the specified <code>String</code> |
|
* native, and creates new mappings to the specified |
|
* <code>DataFlavor</code>s. Unlike <code>getFlavorsForNative</code>, the |
|
* mappings will only be established in one direction, and the natives need |
|
* not be encoded. To establish two-way mappings, call |
|
* <code>setNativesForFlavor</code> as well. The first |
|
* <code>DataFlavor</code> in the array will represent the highest priority |
|
* mapping. Subsequent <code>DataFlavor</code>s will represent mappings of |
|
* decreasing priority. |
|
* <p> |
|
* If the array contains several elements that reference equal |
|
* <code>DataFlavor</code>s, this method will establish new mappings |
|
* for the first of those elements and ignore the rest of them. |
|
* <p> |
|
* It is recommended that client code not reset mappings established by the |
|
* data transfer subsystem. This method should only be used for |
|
* application-level mappings. |
|
* |
|
* @param nat the <code>String</code> native key for the mappings |
|
* @param flavors the <code>DataFlavor</code> values for the mappings |
|
* @throws NullPointerException if nat or flavors is <code>null</code> |
|
* or if flavors contains <code>null</code> elements |
|
* |
|
* @see #setNativesForFlavor |
|
* @since 1.4 |
|
*/ |
|
public synchronized void setFlavorsForNative(String nat, |
|
DataFlavor[] flavors) { |
|
Objects.requireNonNull(nat, "Null native not permitted"); |
|
Objects.requireNonNull(flavors, "Null flavors not permitted"); |
|
getNativeToFlavor().remove(nat); |
|
for (DataFlavor flavor : flavors) { |
|
addFlavorForUnencodedNative(nat, flavor); |
|
} |
|
disabledMappingGenerationKeys.add(nat); |
|
flavorsForNativeCache.remove(nat); |
|
} |
|
/** |
|
* Encodes a MIME type for use as a <code>String</code> native. The format |
|
* of an encoded representation of a MIME type is implementation-dependent. |
|
* The only restrictions are: |
|
* <ul> |
|
* <li>The encoded representation is <code>null</code> if and only if the |
|
* MIME type <code>String</code> is <code>null</code>.</li> |
|
* <li>The encoded representations for two non-<code>null</code> MIME type |
|
* <code>String</code>s are equal if and only if these <code>String</code>s |
|
* are equal according to <code>String.equals(Object)</code>.</li> |
|
* </ul> |
|
* <p> |
|
* The reference implementation of this method returns the specified MIME |
|
* type <code>String</code> prefixed with <code>JAVA_DATAFLAVOR:</code>. |
|
* |
|
* @param mimeType the MIME type to encode |
|
* @return the encoded <code>String</code>, or <code>null</code> if |
|
* mimeType is <code>null</code> |
|
*/ |
|
public static String encodeJavaMIMEType(String mimeType) { |
|
return (mimeType != null) |
|
? JavaMIME + mimeType |
|
: null; |
|
} |
|
/** |
|
* Encodes a <code>DataFlavor</code> for use as a <code>String</code> |
|
* native. The format of an encoded <code>DataFlavor</code> is |
|
* implementation-dependent. The only restrictions are: |
|
* <ul> |
|
* <li>The encoded representation is <code>null</code> if and only if the |
|
* specified <code>DataFlavor</code> is <code>null</code> or its MIME type |
|
* <code>String</code> is <code>null</code>.</li> |
|
* <li>The encoded representations for two non-<code>null</code> |
|
* <code>DataFlavor</code>s with non-<code>null</code> MIME type |
|
* <code>String</code>s are equal if and only if the MIME type |
|
* <code>String</code>s of these <code>DataFlavor</code>s are equal |
|
* according to <code>String.equals(Object)</code>.</li> |
|
* </ul> |
|
* <p> |
|
* The reference implementation of this method returns the MIME type |
|
* <code>String</code> of the specified <code>DataFlavor</code> prefixed |
|
* with <code>JAVA_DATAFLAVOR:</code>. |
|
* |
|
* @param flav the <code>DataFlavor</code> to encode |
|
* @return the encoded <code>String</code>, or <code>null</code> if |
|
* flav is <code>null</code> or has a <code>null</code> MIME type |
|
*/ |
|
public static String encodeDataFlavor(DataFlavor flav) { |
|
return (flav != null) |
|
? SystemFlavorMap.encodeJavaMIMEType(flav.getMimeType()) |
|
: null; |
|
} |
|
/** |
|
* Returns whether the specified <code>String</code> is an encoded Java |
|
* MIME type. |
|
* |
|
* @param str the <code>String</code> to test |
|
* @return <code>true</code> if the <code>String</code> is encoded; |
|
* <code>false</code> otherwise |
|
*/ |
|
public static boolean isJavaMIMEType(String str) { |
|
return (str != null && str.startsWith(JavaMIME, 0)); |
|
} |
|
/** |
|
* Decodes a <code>String</code> native for use as a Java MIME type. |
|
* |
|
* @param nat the <code>String</code> to decode |
|
* @return the decoded Java MIME type, or <code>null</code> if nat is not |
|
* an encoded <code>String</code> native |
|
*/ |
|
public static String decodeJavaMIMEType(String nat) { |
|
return (isJavaMIMEType(nat)) |
|
? nat.substring(JavaMIME.length(), nat.length()).trim() |
|
: null; |
|
} |
|
/** |
|
* Decodes a <code>String</code> native for use as a |
|
* <code>DataFlavor</code>. |
|
* |
|
* @param nat the <code>String</code> to decode |
|
* @return the decoded <code>DataFlavor</code>, or <code>null</code> if |
|
* nat is not an encoded <code>String</code> native |
|
*/ |
|
public static DataFlavor decodeDataFlavor(String nat) |
|
throws ClassNotFoundException |
|
{ |
|
String retval_str = SystemFlavorMap.decodeJavaMIMEType(nat); |
|
return (retval_str != null) |
|
? new DataFlavor(retval_str) |
|
: null; |
|
} |
|
private static final class SoftCache<K, V> { |
|
Map<K, SoftReference<LinkedHashSet<V>>> cache; |
|
public void put(K key, LinkedHashSet<V> value) { |
|
if (cache == null) { |
|
cache = new HashMap<>(1); |
|
} |
|
cache.put(key, new SoftReference<>(value)); |
|
} |
|
public void remove(K key) { |
|
if (cache == null) return; |
|
cache.remove(null); |
|
cache.remove(key); |
|
} |
|
public LinkedHashSet<V> check(K key) { |
|
if (cache == null) return null; |
|
SoftReference<LinkedHashSet<V>> ref = cache.get(key); |
|
if (ref != null) { |
|
return ref.get(); |
|
} |
|
return null; |
|
} |
|
} |
|
} |