1
2/*
3 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5 *
6 * This code is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License version 2 only, as
8 * published by the Free Software Foundation.  Oracle designates this
9 * particular file as subject to the "Classpath" exception as provided
10 * by Oracle in the LICENSE file that accompanied this code.
11 *
12 * This code is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
15 * version 2 for more details (a copy is included in the LICENSE file that
16 * accompanied this code).
17 *
18 * You should have received a copy of the GNU General Public License version
19 * 2 along with this work; if not, write to the Free Software Foundation,
20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21 *
22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23 * or visit www.oracle.com if you need additional information or have any
24 * questions.
25 */
26
27package sun.lwawt.macosx;
28
29import java.awt.*;
30
31import java.io.*;
32import java.net.URI;
33import java.net.URISyntaxException;
34import java.net.URL;
35import java.nio.charset.Charset;
36import java.text.Normalizer;
37import java.text.Normalizer.Form;
38import java.util.*;
39import java.util.regex.*;
40import java.awt.datatransfer.*;
41import java.nio.charset.StandardCharsets;
42import sun.awt.datatransfer.*;
43
44public class CDataTransferer extends DataTransferer {
45    private static final Map<String, Long> predefinedClipboardNameMap;
46    private static final Map<Long, String> predefinedClipboardFormatMap;
47
48    // See SystemFlavorMap, or the flavormap.properties file:
49    // We should define a few more types in flavormap.properties, it's rather slim now.
50    private static final String[] predefinedClipboardNames = {
51        "",
52        "STRING",
53        "FILE_NAME",
54        "TIFF",
55        "RICH_TEXT",
56        "HTML",
57        "PDF",
58        "URL",
59        "PNG",
60        "JFIF",
61        "XPICT"
62    };
63
64    static {
65        Map<String, Long> nameMap = new HashMap<>(predefinedClipboardNames.length, 1.0f);
66        Map<Long, String> formatMap = new HashMap<>(predefinedClipboardNames.length, 1.0f);
67        for (int i = 1; i < predefinedClipboardNames.length; i++) {
68            nameMap.put(predefinedClipboardNames[i], (long) i);
69            formatMap.put((long) i, predefinedClipboardNames[i]);
70        }
71        predefinedClipboardNameMap = Collections.synchronizedMap(nameMap);
72        predefinedClipboardFormatMap = Collections.synchronizedMap(formatMap);
73    }
74
75    public static final int CF_UNSUPPORTED = 0;
76    public static final int CF_STRING      = 1;
77    public static final int CF_FILE        = 2;
78    public static final int CF_TIFF        = 3;
79    public static final int CF_RICH_TEXT   = 4;
80    public static final int CF_HTML        = 5;
81    public static final int CF_PDF         = 6;
82    public static final int CF_URL         = 7;
83    public static final int CF_PNG         = 8;
84    public static final int CF_JPEG        = 9;
85    public static final int CF_XPICT       = 10;
86
87    private CDataTransferer() {}
88
89    private static CDataTransferer fTransferer;
90
91    static synchronized CDataTransferer getInstanceImpl() {
92        if (fTransferer == null) {
93            fTransferer = new CDataTransferer();
94        }
95
96        return fTransferer;
97    }
98
99    @Override
100    public String getDefaultUnicodeEncoding() {
101        return "utf-16le";
102    }
103
104    @Override
105    public boolean isLocaleDependentTextFormat(long format) {
106        return format == CF_STRING;
107    }
108
109    @Override
110    public boolean isFileFormat(long format) {
111        return format == CF_FILE;
112    }
113
114    @Override
115    public boolean isImageFormat(long format) {
116        int ifmt = (int)format;
117        switch(ifmt) {
118            case CF_TIFF:
119            case CF_PDF:
120            case CF_PNG:
121            case CF_JPEG:
122                return true;
123            default:
124                return false;
125        }
126    }
127
128    @Override
129    public Object translateBytes(byte[] bytes, DataFlavor flavor,
130                                 long format, Transferable transferable) throws IOException {
131
132        if (format == CF_URL && URL.class.equals(flavor.getRepresentationClass())) {
133            String charset = Charset.defaultCharset().name();
134            if (transferable != null && transferable.isDataFlavorSupported(javaTextEncodingFlavor)) {
135                try {
136                    charset = new String((byte[]) transferable.getTransferData(javaTextEncodingFlavor), StandardCharsets.UTF_8);
137                } catch (UnsupportedFlavorException cannotHappen) {
138                }
139            }
140
141            String xml = new String(bytes, charset);
142            // macosx pasteboard returns a property list that consists of one URL
143            // let's extract it.
144            return new URL(extractURL(xml));
145        }
146
147        if(isUriListFlavor(flavor) && format == CF_FILE) {
148            // dragQueryFile works fine with files and url,
149            // it parses and extracts values from property list.
150            // maxosx always returns property list for
151            // CF_URL and CF_FILE
152            String[] strings = dragQueryFile(bytes);
153            if(strings == null) {
154                return null;
155            }
156            bytes = String.join(System.getProperty("line.separator"),
157                    strings).getBytes();
158            // now we extracted uri from xml, now we should treat it as
159            // regular string that allows to translate data to target represantation
160            // class by base method
161            format = CF_STRING;
162        } else if (format == CF_STRING) {
163            bytes = Normalizer.normalize(new String(bytes, "UTF8"), Form.NFC).getBytes("UTF8");
164        }
165
166        return super.translateBytes(bytes, flavor, format, transferable);
167    }
168
169    private String extractURL(String xml) {
170       Pattern urlExtractorPattern = Pattern.compile("<string>(.*)</string>");
171        Matcher matcher = urlExtractorPattern.matcher(xml);
172        if (matcher.find()) {
173            return matcher.group(1);
174        } else {
175            return null;
176        }
177    }
178
179    @Override
180    protected synchronized Long getFormatForNativeAsLong(String str) {
181        Long format = predefinedClipboardNameMap.get(str);
182
183        if (format == null) {
184            if (java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance()) {
185                // Do not try to access native system for the unknown format
186                return -1L;
187            }
188            format = registerFormatWithPasteboard(str);
189            predefinedClipboardNameMap.put(str, format);
190            predefinedClipboardFormatMap.put(format, str);
191        }
192
193        return format;
194    }
195
196    /*
197     * Adds type to native mapping NSDictionary.
198     */
199    private native long registerFormatWithPasteboard(String type);
200
201    // Get registered native format string for an index, return null if unknown:
202    private native String formatForIndex(long index);
203
204    @Override
205    protected String getNativeForFormat(long format) {
206        String returnValue = null;
207
208        // The most common case - just index the array of predefined names:
209        if (format >= 0 && format < predefinedClipboardNames.length) {
210            returnValue = predefinedClipboardNames[(int) format];
211        } else {
212            Long formatObj = format;
213            returnValue = predefinedClipboardFormatMap.get(formatObj);
214
215            // predefinedClipboardFormatMap may not know this format:
216            if (returnValue == null) {
217                returnValue = formatForIndex(format);
218
219                // Native clipboard may not know this format either:
220                if (returnValue != null) {
221                    predefinedClipboardNameMap.put(returnValue, formatObj);
222                    predefinedClipboardFormatMap.put(formatObj, returnValue);
223                }
224            }
225        }
226
227        if (returnValue == null) {
228            returnValue = predefinedClipboardNames[CF_UNSUPPORTED];
229        }
230
231        return returnValue;
232    }
233
234    private final ToolkitThreadBlockedHandler handler = new CToolkitThreadBlockedHandler();
235
236    @Override
237    public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
238        return handler;
239    }
240
241    @Override
242    protected byte[] imageToPlatformBytes(Image image, long format) {
243        return CImage.getCreator().getPlatformImageBytes(image);
244    }
245
246    private static native String[] nativeDragQueryFile(final byte[] bytes);
247    @Override
248    protected String[] dragQueryFile(final byte[] bytes) {
249        if (bytes == null) return null;
250        if (new String(bytes).startsWith("Unsupported type")) return null;
251        return nativeDragQueryFile(bytes);
252    }
253
254
255    @Override
256    protected Image platformImageBytesToImage(byte[] bytes, long format) throws IOException {
257        return CImage.getCreator().createImageFromPlatformImageBytes(bytes);
258    }
259
260    @Override
261    protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList) throws IOException {
262        ByteArrayOutputStream bos = new ByteArrayOutputStream();
263        for (String file : fileList) {
264            byte[] bytes = file.getBytes();
265            bos.write(bytes, 0, bytes.length);
266            bos.write(0);
267        }
268        return bos;
269    }
270
271    @Override
272    protected boolean isURIListFormat(long format) {
273        String nat = getNativeForFormat(format);
274        if (nat == null) {
275            return false;
276        }
277        try {
278            DataFlavor df = new DataFlavor(nat);
279            if (isUriListFlavor(df)) {
280                return true;
281            }
282        } catch (Exception e) {
283            // Not a MIME format.
284        }
285        return false;
286    }
287
288    private boolean isUriListFlavor(DataFlavor df) {
289        if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) {
290            return true;
291        }
292        return false;
293    }
294}
295