/*
 * Copyright (c) 2011, 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 sun.lwawt.macosx;

import java.awt.*;
import java.awt.image.*;
import sun.awt.image.ImageRepresentation;

import java.io.*;
import java.net.URL;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.*;

import java.awt.datatransfer.*;
import sun.awt.datatransfer.*;

public class CDataTransferer extends DataTransferer {
    private static final Map<String, Long> predefinedClipboardNameMap;
    private static final Map<Long, String> predefinedClipboardFormatMap;

    // See SystemFlavorMap, or the flavormap.properties file:
    // We should define a few more types in flavormap.properties, it's rather slim now.
    private static final String[] predefinedClipboardNames = {
        "",
        "STRING",
        "FILE_NAME",
        "TIFF",
        "RICH_TEXT",
        "HTML",
        "PDF",
        "URL"
    };

    static {
        Map<String, Long> nameMap = new HashMap<String, Long>(predefinedClipboardNames.length, 1.0f);
        Map<Long, String> formatMap = new HashMap<Long, String>(predefinedClipboardNames.length, 1.0f);
        for (int i = 1; i < predefinedClipboardNames.length; i++) {
            nameMap.put(predefinedClipboardNames[i], new Long(i));
            formatMap.put(new Long(i), predefinedClipboardNames[i]);
        }
        predefinedClipboardNameMap = Collections.synchronizedMap(nameMap);
        predefinedClipboardFormatMap = Collections.synchronizedMap(formatMap);
    }

    public static final int CF_UNSUPPORTED = 0;
    public static final int CF_STRING      = 1;
    public static final int CF_FILE        = 2;
    public static final int CF_TIFF        = 3;
    public static final int CF_RICH_TEXT   = 4;
    public static final int CF_HTML        = 5;
    public static final int CF_PDF         = 6;
    public static final int CF_URL         = 7;
    public static final int CF_PNG         = 10;
    public static final int CF_JPEG        = 11;

    public static final Long L_CF_TIFF = predefinedClipboardNameMap.get(predefinedClipboardNames[CF_TIFF]);

    // Image file formats with java.awt.Image representation:
    private static final Long[] imageFormats = new Long[] {
        L_CF_TIFF
    };


    private CDataTransferer() {}

    private static CDataTransferer fTransferer;

    public static synchronized CDataTransferer getInstanceImpl() {
        if (fTransferer == null) {
            fTransferer = new CDataTransferer();
        }

        return fTransferer;
    }

    public String getDefaultUnicodeEncoding() {
        return "utf-16le";
    }

    public boolean isLocaleDependentTextFormat(long format) {
        return format == CF_STRING;
    }

    public boolean isFileFormat(long format) {
        return format == CF_FILE;
    }

    public boolean isImageFormat(long format) {
        int ifmt = (int)format;
        switch(ifmt) {
            case CF_TIFF:
            case CF_PDF:
            case CF_PNG:
            case CF_JPEG:
                return true;
            default:
                return false;
        }
    }

    protected Long[] getImageFormatsAsLongArray() {
        return imageFormats;
    }

    public byte[] translateTransferable(Transferable contents, DataFlavor flavor, long format) throws IOException
        {
            byte[] bytes = super.translateTransferable(contents, flavor, format);

            // 9-12-02 VL: we may need to do something like Windows here.
            //if (format == CF_HTML) {
            //    bytes = HTMLSupport.convertToHTMLFormat(bytes);
            //}

            return bytes;
        }

    protected Object translateBytesOrStream(InputStream stream, byte[] bytes, DataFlavor flavor, long format,
                                            Transferable transferable) throws IOException
        {
            // 5-28-03 VL: [Radar 3266030]
            // We need to do like Windows does here.
            if (format == CF_HTML && flavor.isFlavorTextType()) {
                if (stream == null) {
                    stream = new ByteArrayInputStream(bytes);
                    bytes = null;
                }

                stream = new HTMLDecodingInputStream(stream);
            }

            if (format == CF_URL && URL.class.equals(flavor.getRepresentationClass()))
            {
                if (bytes == null) {
                    bytes = inputStreamToByteArray(stream);
                    stream = null;
                }

                String charset = getDefaultTextCharset();
                if (transferable != null && transferable.isDataFlavorSupported(javaTextEncodingFlavor)) {
                    try {
                        charset = new String((byte[])transferable.getTransferData(javaTextEncodingFlavor), "UTF-8");
                    } catch (UnsupportedFlavorException cannotHappen) {
                    }
                }

                return new URL(new String(bytes, charset));
            }

            if (format == CF_STRING) {
                bytes = Normalizer.normalize(new String(bytes, "UTF8"), Form.NFC).getBytes("UTF8");
            }

            return super.translateBytesOrStream(stream, bytes, flavor, format, transferable);
        }


    synchronized protected Long getFormatForNativeAsLong(String str) {
        Long format = predefinedClipboardNameMap.get(str);

        if (format == null) {
            if (java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance()) {
                // Do not try to access GUI manager for unknown format
                return new Long(-1);
            }
            format = new Long(registerFormatWithPasteboard(str));
            predefinedClipboardNameMap.put(str, format);
            predefinedClipboardFormatMap.put(format, str);
        }

        return format;
    }

    /*
     * Adds type to native mapping NSDictionary.
     */
    private native long registerFormatWithPasteboard(String type);

    // Get registered native format string for an index, return null if unknown:
    private native String formatForIndex(long index);

    protected String getNativeForFormat(long format) {
        String returnValue = null;

        // The most common case - just index the array of predefined names:
        if (format >= 0 && format < predefinedClipboardNames.length) {
            returnValue = predefinedClipboardNames[(int) format];
        } else {
            Long formatObj = new Long(format);
            returnValue = predefinedClipboardFormatMap.get(formatObj);

            // predefinedClipboardFormatMap may not know this format:
            if (returnValue == null) {
                returnValue = formatForIndex(format);

                // Native clipboard may not know this format either:
                if (returnValue != null) {
                    predefinedClipboardNameMap.put(returnValue, formatObj);
                    predefinedClipboardFormatMap.put(formatObj, returnValue);
                }
            }
        }

        if (returnValue == null) {
            returnValue = predefinedClipboardNames[CF_UNSUPPORTED];
        }

        return returnValue;
    }

    private final ToolkitThreadBlockedHandler handler = new CToolkitThreadBlockedHandler();

    public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
        return handler;
    }

    protected byte[] imageToPlatformBytes(Image image, long format) {
        int w = image.getWidth(null);
        int h = image.getHeight(null);
        BufferedImage bimage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
        Graphics g = bimage.getGraphics();
        g.drawImage(image, 0, 0, w, h, null);
        g.dispose();
        Raster raster = bimage.getRaster();
        DataBuffer buffer = raster.getDataBuffer();
        return imageDataToPlatformImageBytes(((DataBufferInt)buffer).getData(),
                                             raster.getWidth(),
                                             raster.getHeight());
    }

    private static native String[] nativeDragQueryFile(final byte[] bytes);
    protected String[] dragQueryFile(final byte[] bytes) {
        if (bytes == null) return null;
        if (new String(bytes).startsWith("Unsupported type")) return null;
        return nativeDragQueryFile(bytes);
    }

    private native byte[] imageDataToPlatformImageBytes(int[] rData, int nW, int nH);

    
Translates either a byte array or an input stream which contain platform-specific image data in the given format into an Image.
/** * Translates either a byte array or an input stream which contain * platform-specific image data in the given format into an Image. */
protected Image platformImageBytesOrStreamToImage(InputStream stream, byte[] bytes, long format) throws IOException { byte[] imageData = bytes; if (imageData == null) imageData = inputStreamToByteArray(stream); return getImageForByteStream(imageData); } private native Image getImageForByteStream(byte[] bytes); @Override protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (int i = 0; i < fileList.size(); i++) { byte[] bytes = fileList.get(i).getBytes(); bos.write(bytes, 0, bytes.length); bos.write(0); } return bos; } @Override protected boolean isURIListFormat(long format) { String nat = getNativeForFormat(format); if (nat == null) { return false; } try { DataFlavor df = new DataFlavor(nat); if (df.getPrimaryType().equals("text") && df.getSubType().equals("uri-list")) { return true; } } catch (Exception e) { // Not a MIME format. } return false; } } // ---- Code borrowed from WDataTransferer: ---- // This will come handy for supporting HTML data. final class HTMLSupport { public static final String ENCODING = "UTF-8"; public static final String VERSION = "Version:"; public static final String START_HTML = "StartHTML:"; public static final String END_HTML = "EndHTML:"; public static final String START_FRAGMENT = "StartFragment:"; public static final String END_FRAGMENT = "EndFragment:"; public static final String START_FRAGMENT_CMT = "<!--StartFragment-->"; public static final String END_FRAGMENT_CMT = "<!--EndFragment-->"; public static final String EOLN = "\r\n"; private static final String VERSION_NUM = "0.9"; private static final String HTML_START_END = "-1"; private static final int PADDED_WIDTH = 10; private static final int HEADER_LEN = VERSION.length() + VERSION_NUM.length() + EOLN.length() + START_HTML.length() + HTML_START_END.length() + EOLN.length() + END_HTML.length() + HTML_START_END.length() + EOLN.length() + START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() + END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length() + START_FRAGMENT_CMT.length() + EOLN.length(); private static final String HEADER_LEN_STR = toPaddedString(HEADER_LEN, PADDED_WIDTH); private static final String TRAILER = END_FRAGMENT_CMT + EOLN + '\0'; private static String toPaddedString(int n, int width) { String string = "" + n; int len = string.length(); if (n >= 0 && len < width) { char[] array = new char[width - len]; Arrays.fill(array, '0'); StringBuffer buffer = new StringBuffer(); buffer.append(array); buffer.append(string); string = buffer.toString(); } return string; } public static byte[] convertToHTMLFormat(byte[] bytes) { StringBuffer header = new StringBuffer(HEADER_LEN); header.append(VERSION); header.append(VERSION_NUM); header.append(EOLN); header.append(START_HTML); header.append(HTML_START_END); header.append(EOLN); header.append(END_HTML); header.append(HTML_START_END); header.append(EOLN); header.append(START_FRAGMENT); header.append(HEADER_LEN_STR); header.append(EOLN); header.append(END_FRAGMENT); // Strip terminating NUL byte from array header.append(toPaddedString(HEADER_LEN + bytes.length - 1, PADDED_WIDTH)); header.append(EOLN); header.append(START_FRAGMENT_CMT); header.append(EOLN); byte[] headerBytes = null, trailerBytes = null; try { headerBytes = new String(header).getBytes(ENCODING); trailerBytes = TRAILER.getBytes(ENCODING); } catch (UnsupportedEncodingException cannotHappen) { } byte[] retval = new byte[headerBytes.length + bytes.length - 1 + trailerBytes.length]; System.arraycopy(headerBytes, 0, retval, 0, headerBytes.length); System.arraycopy(bytes, 0, retval, headerBytes.length, bytes.length - 1); System.arraycopy(trailerBytes, 0, retval, headerBytes.length + bytes.length - 1, trailerBytes.length); return retval; } }
This stream takes an InputStream which provides data in CF_HTML format, strips off the description and context to extract the original HTML data.
/** * This stream takes an InputStream which provides data in CF_HTML format, * strips off the description and context to extract the original HTML data. */
class HTMLDecodingInputStream extends InputStream { private final BufferedInputStream bufferedStream; private boolean descriptionParsed = false; private boolean closed = false; private int index; private int end; // InputStreamReader uses an 8K buffer. The size is not customizable. public static final int BYTE_BUFFER_LEN = 8192; // CharToByteUTF8.getMaxBytesPerChar returns 3, so we should not buffer // more chars than 3 times the number of bytes we can buffer. public static final int CHAR_BUFFER_LEN = BYTE_BUFFER_LEN / 3; private static final String FAILURE_MSG = "Unable to parse HTML description: "; private static final String INVALID_MSG = " invalid"; public HTMLDecodingInputStream(InputStream bytestream) throws IOException { bufferedStream = new BufferedInputStream(bytestream, BYTE_BUFFER_LEN); } private void parseDescription() throws IOException { bufferedStream.mark(BYTE_BUFFER_LEN); BufferedReader bufferedReader = new BufferedReader (new InputStreamReader(bufferedStream, HTMLSupport.ENCODING), CHAR_BUFFER_LEN); String version = bufferedReader.readLine().trim(); if (version == null || !version.startsWith(HTMLSupport.VERSION)) { // Not MS-compliant HTML text. Return raw text from read(). index = 0; end = -1; bufferedStream.reset(); return; } String input; boolean startHTML, endHTML, startFragment, endFragment; startHTML = endHTML = startFragment = endFragment = false; try { do { input = bufferedReader.readLine().trim(); if (input == null) { close(); throw new IOException(FAILURE_MSG); } else if (input.startsWith(HTMLSupport.START_HTML)) { int val = Integer.parseInt (input.substring(HTMLSupport.START_HTML.length(), input.length()).trim()); if (val >= 0) { index = val; startHTML = true; } else if (val != -1) { close(); throw new IOException(FAILURE_MSG + HTMLSupport.START_HTML + INVALID_MSG); } } else if (input.startsWith(HTMLSupport.END_HTML)) { int val = Integer.parseInt (input.substring(HTMLSupport.END_HTML.length(), input.length()).trim()); if (val >= 0) { end = val; endHTML = true; } else if (val != -1) { close(); throw new IOException(FAILURE_MSG + HTMLSupport.END_HTML + INVALID_MSG); } } else if (!startHTML && !endHTML && input.startsWith(HTMLSupport.START_FRAGMENT)) { index = Integer.parseInt (input.substring(HTMLSupport.START_FRAGMENT.length(), input.length()).trim()); if (index < 0) { close(); throw new IOException(FAILURE_MSG + HTMLSupport.START_FRAGMENT + INVALID_MSG); } startFragment = true; } else if (!startHTML && !endHTML && input.startsWith(HTMLSupport.END_FRAGMENT)) { end = Integer.parseInt (input.substring(HTMLSupport.END_FRAGMENT.length(), input.length()).trim()); if (end < 0) { close(); throw new IOException(FAILURE_MSG + HTMLSupport.END_FRAGMENT + INVALID_MSG); } endFragment = true; } } while (!((startHTML && endHTML) || (startFragment && endFragment))); } catch (NumberFormatException e) { close(); throw new IOException(FAILURE_MSG + e); } bufferedStream.reset(); for (int i = 0; i < index; i++) { if (bufferedStream.read() == -1) { close(); throw new IOException(FAILURE_MSG + "Byte stream ends in description."); } } } public int read() throws IOException { if (closed) { throw new IOException("Stream closed"); } if (!descriptionParsed) { parseDescription(); // initializes 'index' and 'end' descriptionParsed = true; } if (end != -1 && index >= end) { return -1; } int retval = bufferedStream.read(); if (retval == -1) { index = end = 0; // so future read() calls will fail quickly return -1; } index++; // System.out.print((char)retval); return retval; } public void close() throws IOException { if (!closed) { closed = true; bufferedStream.close(); } } }