924 lines
32 KiB
Java
924 lines
32 KiB
Java
/*
|
|
* Copyright (c) 2000, 2014, 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.awt.windows;
|
|
|
|
import java.awt.Image;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Transparency;
|
|
|
|
import java.awt.color.ColorSpace;
|
|
|
|
import java.awt.datatransfer.DataFlavor;
|
|
import java.awt.datatransfer.FlavorTable;
|
|
import java.awt.datatransfer.Transferable;
|
|
import java.awt.datatransfer.UnsupportedFlavorException;
|
|
|
|
import java.awt.geom.AffineTransform;
|
|
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.ColorModel;
|
|
import java.awt.image.ComponentColorModel;
|
|
import java.awt.image.DataBuffer;
|
|
import java.awt.image.DataBufferByte;
|
|
import java.awt.image.DataBufferInt;
|
|
import java.awt.image.DirectColorModel;
|
|
import java.awt.image.ImageObserver;
|
|
import java.awt.image.Raster;
|
|
import java.awt.image.WritableRaster;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.BufferedReader;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.IOException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.io.File;
|
|
|
|
import java.net.URL;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.SortedMap;
|
|
|
|
import sun.awt.Mutex;
|
|
import sun.awt.datatransfer.DataTransferer;
|
|
import sun.awt.datatransfer.ToolkitThreadBlockedHandler;
|
|
|
|
import sun.awt.image.ImageRepresentation;
|
|
import sun.awt.image.ToolkitImage;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
/**
|
|
* Platform-specific support for the data transfer subsystem.
|
|
*
|
|
* @author David Mendenhall
|
|
* @author Danila Sinopalnikov
|
|
*
|
|
* @since 1.3.1
|
|
*/
|
|
final class WDataTransferer extends DataTransferer {
|
|
private static final String[] predefinedClipboardNames = {
|
|
"",
|
|
"TEXT",
|
|
"BITMAP",
|
|
"METAFILEPICT",
|
|
"SYLK",
|
|
"DIF",
|
|
"TIFF",
|
|
"OEM TEXT",
|
|
"DIB",
|
|
"PALETTE",
|
|
"PENDATA",
|
|
"RIFF",
|
|
"WAVE",
|
|
"UNICODE TEXT",
|
|
"ENHMETAFILE",
|
|
"HDROP",
|
|
"LOCALE",
|
|
"DIBV5"
|
|
};
|
|
|
|
private static final Map <String, Long> predefinedClipboardNameMap;
|
|
static {
|
|
Map <String,Long> tempMap =
|
|
new HashMap <> (predefinedClipboardNames.length, 1.0f);
|
|
for (int i = 1; i < predefinedClipboardNames.length; i++) {
|
|
tempMap.put(predefinedClipboardNames[i], Long.valueOf(i));
|
|
}
|
|
predefinedClipboardNameMap =
|
|
Collections.synchronizedMap(tempMap);
|
|
}
|
|
|
|
/**
|
|
* from winuser.h
|
|
*/
|
|
public static final int CF_TEXT = 1;
|
|
public static final int CF_METAFILEPICT = 3;
|
|
public static final int CF_DIB = 8;
|
|
public static final int CF_ENHMETAFILE = 14;
|
|
public static final int CF_HDROP = 15;
|
|
public static final int CF_LOCALE = 16;
|
|
|
|
public static final long CF_HTML = registerClipboardFormat("HTML Format");
|
|
public static final long CFSTR_INETURL = registerClipboardFormat("UniformResourceLocator");
|
|
public static final long CF_PNG = registerClipboardFormat("PNG");
|
|
public static final long CF_JFIF = registerClipboardFormat("JFIF");
|
|
|
|
public static final long CF_FILEGROUPDESCRIPTORW = registerClipboardFormat("FileGroupDescriptorW");
|
|
public static final long CF_FILEGROUPDESCRIPTORA = registerClipboardFormat("FileGroupDescriptor");
|
|
//CF_FILECONTENTS supported as mandatory associated clipboard
|
|
|
|
private static final Long L_CF_LOCALE =
|
|
predefinedClipboardNameMap.get(predefinedClipboardNames[CF_LOCALE]);
|
|
|
|
private static final DirectColorModel directColorModel =
|
|
new DirectColorModel(24,
|
|
0x00FF0000, /* red mask */
|
|
0x0000FF00, /* green mask */
|
|
0x000000FF); /* blue mask */
|
|
|
|
private static final int[] bandmasks = new int[] {
|
|
directColorModel.getRedMask(),
|
|
directColorModel.getGreenMask(),
|
|
directColorModel.getBlueMask() };
|
|
|
|
/**
|
|
* Singleton constructor
|
|
*/
|
|
private WDataTransferer() {
|
|
}
|
|
|
|
private static WDataTransferer transferer;
|
|
|
|
static synchronized WDataTransferer getInstanceImpl() {
|
|
if (transferer == null) {
|
|
transferer = new WDataTransferer();
|
|
}
|
|
return transferer;
|
|
}
|
|
|
|
@Override
|
|
public SortedMap <Long, DataFlavor> getFormatsForFlavors(
|
|
DataFlavor[] flavors, FlavorTable map)
|
|
{
|
|
SortedMap <Long, DataFlavor> retval =
|
|
super.getFormatsForFlavors(flavors, map);
|
|
|
|
// The Win32 native code does not support exporting LOCALE data, nor
|
|
// should it.
|
|
retval.remove(L_CF_LOCALE);
|
|
|
|
return retval;
|
|
}
|
|
|
|
@Override
|
|
public String getDefaultUnicodeEncoding() {
|
|
return "utf-16le";
|
|
}
|
|
|
|
@Override
|
|
public byte[] translateTransferable(Transferable contents,
|
|
DataFlavor flavor,
|
|
long format) throws IOException
|
|
{
|
|
byte[] bytes = null;
|
|
if (format == CF_HTML) {
|
|
if (contents.isDataFlavorSupported(DataFlavor.selectionHtmlFlavor)) {
|
|
// if a user provides data represented by
|
|
// DataFlavor.selectionHtmlFlavor format, we use this
|
|
// type to store the data in the native clipboard
|
|
bytes = super.translateTransferable(contents,
|
|
DataFlavor.selectionHtmlFlavor,
|
|
format);
|
|
} else if (contents.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) {
|
|
// if we cannot get data represented by the
|
|
// DataFlavor.selectionHtmlFlavor format
|
|
// but the DataFlavor.allHtmlFlavor format is avialable
|
|
// we belive that the user knows how to represent
|
|
// the data and how to mark up selection in a
|
|
// system specific manner. Therefor, we use this data
|
|
bytes = super.translateTransferable(contents,
|
|
DataFlavor.allHtmlFlavor,
|
|
format);
|
|
} else {
|
|
// handle other html flavor types, including custom and
|
|
// fragment ones
|
|
bytes = HTMLCodec.convertToHTMLFormat(super.translateTransferable(contents, flavor, format));
|
|
}
|
|
} else {
|
|
// we handle non-html types basing on their
|
|
// flavors
|
|
bytes = super.translateTransferable(contents, flavor, format);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
// The stream is closed as a closable object
|
|
@Override
|
|
public Object translateStream(InputStream str,
|
|
DataFlavor flavor, long format,
|
|
Transferable localeTransferable)
|
|
throws IOException
|
|
{
|
|
if (format == CF_HTML && flavor.isFlavorTextType()) {
|
|
str = new HTMLCodec(str,
|
|
EHTMLReadMode.getEHTMLReadMode(flavor));
|
|
|
|
}
|
|
return super.translateStream(str, flavor, format,
|
|
localeTransferable);
|
|
|
|
}
|
|
|
|
@Override
|
|
public Object translateBytes(byte[] bytes, DataFlavor flavor, long format,
|
|
Transferable localeTransferable) throws IOException
|
|
{
|
|
|
|
|
|
if (format == CF_FILEGROUPDESCRIPTORA || format == CF_FILEGROUPDESCRIPTORW) {
|
|
if (bytes == null || !DataFlavor.javaFileListFlavor.equals(flavor)) {
|
|
throw new IOException("data translation failed");
|
|
}
|
|
String st = new String(bytes, 0, bytes.length, "UTF-16LE");
|
|
String[] filenames = st.split("\0");
|
|
if( 0 == filenames.length ){
|
|
return null;
|
|
}
|
|
|
|
// Convert the strings to File objects
|
|
File[] files = new File[filenames.length];
|
|
for (int i = 0; i < filenames.length; ++i) {
|
|
files[i] = new File(filenames[i]);
|
|
//They are temp-files from memory Stream, so they have to be removed on exit
|
|
files[i].deleteOnExit();
|
|
}
|
|
// Turn the list of Files into a List and return
|
|
return Arrays.asList(files);
|
|
}
|
|
|
|
if (format == CFSTR_INETURL &&
|
|
URL.class.equals(flavor.getRepresentationClass()))
|
|
{
|
|
String charset = getDefaultTextCharset();
|
|
if (localeTransferable != null && localeTransferable.
|
|
isDataFlavorSupported(javaTextEncodingFlavor))
|
|
{
|
|
try {
|
|
charset = new String((byte[])localeTransferable.
|
|
getTransferData(javaTextEncodingFlavor), "UTF-8");
|
|
} catch (UnsupportedFlavorException cannotHappen) {
|
|
}
|
|
}
|
|
return new URL(new String(bytes, charset));
|
|
}
|
|
|
|
return super.translateBytes(bytes , flavor, format,
|
|
localeTransferable);
|
|
|
|
}
|
|
|
|
@Override
|
|
public boolean isLocaleDependentTextFormat(long format) {
|
|
return format == CF_TEXT || format == CFSTR_INETURL;
|
|
}
|
|
|
|
@Override
|
|
public boolean isFileFormat(long format) {
|
|
return format == CF_HDROP || format == CF_FILEGROUPDESCRIPTORA || format == CF_FILEGROUPDESCRIPTORW;
|
|
}
|
|
|
|
@Override
|
|
protected Long getFormatForNativeAsLong(String str) {
|
|
Long format = predefinedClipboardNameMap.get(str);
|
|
if (format == null) {
|
|
format = Long.valueOf(registerClipboardFormat(str));
|
|
}
|
|
return format;
|
|
}
|
|
|
|
@Override
|
|
protected String getNativeForFormat(long format) {
|
|
return (format < predefinedClipboardNames.length)
|
|
? predefinedClipboardNames[(int)format]
|
|
: getClipboardFormatName(format);
|
|
}
|
|
|
|
private final ToolkitThreadBlockedHandler handler =
|
|
new WToolkitThreadBlockedHandler();
|
|
|
|
@Override
|
|
public ToolkitThreadBlockedHandler getToolkitThreadBlockedHandler() {
|
|
return handler;
|
|
}
|
|
|
|
/**
|
|
* Calls the Win32 RegisterClipboardFormat function to register
|
|
* a non-standard format.
|
|
*/
|
|
private static native long registerClipboardFormat(String str);
|
|
|
|
/**
|
|
* Calls the Win32 GetClipboardFormatName function which is
|
|
* the reverse operation of RegisterClipboardFormat.
|
|
*/
|
|
private static native String getClipboardFormatName(long format);
|
|
|
|
@Override
|
|
public boolean isImageFormat(long format) {
|
|
return format == CF_DIB || format == CF_ENHMETAFILE ||
|
|
format == CF_METAFILEPICT || format == CF_PNG ||
|
|
format == CF_JFIF;
|
|
}
|
|
|
|
@Override
|
|
protected byte[] imageToPlatformBytes(Image image, long format)
|
|
throws IOException {
|
|
String mimeType = null;
|
|
if (format == CF_PNG) {
|
|
mimeType = "image/png";
|
|
} else if (format == CF_JFIF) {
|
|
mimeType = "image/jpeg";
|
|
}
|
|
if (mimeType != null) {
|
|
return imageToStandardBytes(image, mimeType);
|
|
}
|
|
|
|
int width = 0;
|
|
int height = 0;
|
|
|
|
if (image instanceof ToolkitImage) {
|
|
ImageRepresentation ir = ((ToolkitImage)image).getImageRep();
|
|
ir.reconstruct(ImageObserver.ALLBITS);
|
|
width = ir.getWidth();
|
|
height = ir.getHeight();
|
|
} else {
|
|
width = image.getWidth(null);
|
|
height = image.getHeight(null);
|
|
}
|
|
|
|
// Fix for 4919639.
|
|
// Some Windows native applications (e.g. clipbrd.exe) do not handle
|
|
// 32-bpp DIBs correctly.
|
|
// As a workaround we switched to 24-bpp DIBs.
|
|
// MSDN prescribes that the bitmap array for a 24-bpp should consist of
|
|
// 3-byte triplets representing blue, green and red components of a
|
|
// pixel respectively. Additionally each scan line must be padded with
|
|
// zeroes to end on a LONG data-type boundary. LONG is always 32-bit.
|
|
// We render the given Image to a BufferedImage of type TYPE_3BYTE_BGR
|
|
// with non-default scanline stride and pass the resulting data buffer
|
|
// to the native code to fill the BITMAPINFO structure.
|
|
int mod = (width * 3) % 4;
|
|
int pad = mod > 0 ? 4 - mod : 0;
|
|
|
|
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
|
int[] nBits = {8, 8, 8};
|
|
int[] bOffs = {2, 1, 0};
|
|
ColorModel colorModel =
|
|
new ComponentColorModel(cs, nBits, false, false,
|
|
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
|
WritableRaster raster =
|
|
Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height,
|
|
width * 3 + pad, 3, bOffs, null);
|
|
|
|
BufferedImage bimage = new BufferedImage(colorModel, raster, false, null);
|
|
|
|
// Some Windows native applications (e.g. clipbrd.exe) do not understand
|
|
// top-down DIBs.
|
|
// So we flip the image vertically and create a bottom-up DIB.
|
|
AffineTransform imageFlipTransform =
|
|
new AffineTransform(1, 0, 0, -1, 0, height);
|
|
|
|
Graphics2D g2d = bimage.createGraphics();
|
|
|
|
try {
|
|
g2d.drawImage(image, imageFlipTransform, null);
|
|
} finally {
|
|
g2d.dispose();
|
|
}
|
|
|
|
DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer();
|
|
|
|
byte[] imageData = buffer.getData();
|
|
return imageDataToPlatformImageBytes(imageData, width, height, format);
|
|
}
|
|
|
|
private static final byte [] UNICODE_NULL_TERMINATOR = new byte [] {0,0};
|
|
|
|
@Override
|
|
protected ByteArrayOutputStream convertFileListToBytes(ArrayList<String> fileList)
|
|
throws IOException
|
|
{
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
|
|
if(fileList.isEmpty()) {
|
|
//store empty unicode string (null terminator)
|
|
bos.write(UNICODE_NULL_TERMINATOR);
|
|
} else {
|
|
for (int i = 0; i < fileList.size(); i++) {
|
|
byte[] bytes = fileList.get(i).getBytes(getDefaultUnicodeEncoding());
|
|
//store unicode string with null terminator
|
|
bos.write(bytes, 0, bytes.length);
|
|
bos.write(UNICODE_NULL_TERMINATOR);
|
|
}
|
|
}
|
|
|
|
// According to MSDN the byte array have to be double NULL-terminated.
|
|
// The array contains Unicode characters, so each NULL-terminator is
|
|
// a pair of bytes
|
|
|
|
bos.write(UNICODE_NULL_TERMINATOR);
|
|
return bos;
|
|
}
|
|
|
|
/**
|
|
* Returns a byte array which contains data special for the given format
|
|
* and for the given image data.
|
|
*/
|
|
private native byte[] imageDataToPlatformImageBytes(byte[] imageData,
|
|
int width, int height,
|
|
long format);
|
|
|
|
/**
|
|
* Translates either a byte array or an input stream which contain
|
|
* platform-specific image data in the given format into an Image.
|
|
*/
|
|
@Override
|
|
protected Image platformImageBytesToImage(byte[] bytes, long format)
|
|
throws IOException {
|
|
String mimeType = null;
|
|
if (format == CF_PNG) {
|
|
mimeType = "image/png";
|
|
} else if (format == CF_JFIF) {
|
|
mimeType = "image/jpeg";
|
|
}
|
|
if (mimeType != null) {
|
|
return standardImageBytesToImage(bytes, mimeType);
|
|
}
|
|
|
|
int[] imageData = platformImageBytesToImageData(bytes, format);
|
|
if (imageData == null) {
|
|
throw new IOException("data translation failed");
|
|
}
|
|
|
|
int len = imageData.length - 2;
|
|
int width = imageData[len];
|
|
int height = imageData[len + 1];
|
|
|
|
DataBufferInt buffer = new DataBufferInt(imageData, len);
|
|
WritableRaster raster = Raster.createPackedRaster(buffer, width,
|
|
height, width,
|
|
bandmasks, null);
|
|
|
|
return new BufferedImage(directColorModel, raster, false, null);
|
|
}
|
|
|
|
/**
|
|
* Translates a byte array which contains platform-specific image data in
|
|
* the given format into an integer array which contains pixel values in
|
|
* ARGB format. The two last elements in the array specify width and
|
|
* height of the image respectively.
|
|
*/
|
|
private native int[] platformImageBytesToImageData(byte[] bytes,
|
|
long format)
|
|
throws IOException;
|
|
|
|
@Override
|
|
protected native String[] dragQueryFile(byte[] bytes);
|
|
}
|
|
|
|
final class WToolkitThreadBlockedHandler extends Mutex
|
|
implements ToolkitThreadBlockedHandler {
|
|
|
|
@Override
|
|
public void enter() {
|
|
if (!isOwned()) {
|
|
throw new IllegalMonitorStateException();
|
|
}
|
|
unlock();
|
|
startSecondaryEventLoop();
|
|
lock();
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
if (!isOwned()) {
|
|
throw new IllegalMonitorStateException();
|
|
}
|
|
WToolkit.quitSecondaryEventLoop();
|
|
}
|
|
|
|
private native void startSecondaryEventLoop();
|
|
}
|
|
|
|
enum EHTMLReadMode {
|
|
HTML_READ_ALL,
|
|
HTML_READ_FRAGMENT,
|
|
HTML_READ_SELECTION;
|
|
|
|
public static EHTMLReadMode getEHTMLReadMode (DataFlavor df) {
|
|
|
|
EHTMLReadMode mode = HTML_READ_SELECTION;
|
|
|
|
String parameter = df.getParameter("document");
|
|
|
|
if ("all".equals(parameter)) {
|
|
mode = HTML_READ_ALL;
|
|
} else if ("fragment".equals(parameter)) {
|
|
mode = HTML_READ_FRAGMENT;
|
|
}
|
|
|
|
return mode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* on decode: This stream takes an InputStream which provides data in CF_HTML format,
|
|
* strips off the description and context to extract the original HTML data.
|
|
*
|
|
* on encode: static convertToHTMLFormat is responsible for HTML clipboard header creation
|
|
*/
|
|
class HTMLCodec extends InputStream {
|
|
//static section
|
|
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_SELECTION = "StartSelection:"; //optional
|
|
public static final String END_SELECTION = "EndSelection:"; //optional
|
|
|
|
public static final String START_FRAGMENT_CMT = "<!--StartFragment-->";
|
|
public static final String END_FRAGMENT_CMT = "<!--EndFragment-->";
|
|
public static final String SOURCE_URL = "SourceURL:";
|
|
public static final String DEF_SOURCE_URL = "about:blank";
|
|
|
|
public static final String EOLN = "\r\n";
|
|
|
|
private static final String VERSION_NUM = "1.0";
|
|
private static final int PADDED_WIDTH = 10;
|
|
|
|
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(width);
|
|
buffer.append(array);
|
|
buffer.append(string);
|
|
string = buffer.toString();
|
|
}
|
|
return string;
|
|
}
|
|
|
|
/**
|
|
* convertToHTMLFormat adds the MS HTML clipboard header to byte array that
|
|
* contains the parameters pairs.
|
|
*
|
|
* The consequence of parameters is fixed, but some or all of them could be
|
|
* omitted. One parameter per one text line.
|
|
* It looks like that:
|
|
*
|
|
* Version:1.0\r\n -- current supported version
|
|
* StartHTML:000000192\r\n -- shift in array to the first byte after the header
|
|
* EndHTML:000000757\r\n -- shift in array of last byte for HTML syntax analysis
|
|
* StartFragment:000000396\r\n -- shift in array jast after <!--StartFragment-->
|
|
* EndFragment:000000694\r\n -- shift in array before start <!--EndFragment-->
|
|
* StartSelection:000000398\r\n -- shift in array of the first char in copied selection
|
|
* EndSelection:000000692\r\n -- shift in array of the last char in copied selection
|
|
* SourceURL:http://sun.com/\r\n -- base URL for related referenses
|
|
* <HTML>...<BODY>...<!--StartFragment-->.....................<!--EndFragment-->...</BODY><HTML>
|
|
* ^ ^ ^ ^^ ^
|
|
* \ StartHTML | \-StartSelection | \EndFragment EndHTML/
|
|
* \-StartFragment \EndSelection
|
|
*
|
|
*Combinations with tags sequence
|
|
*<!--StartFragment--><HTML>...<BODY>...</BODY><HTML><!--EndFragment-->
|
|
* or
|
|
*<HTML>...<!--StartFragment-->...<BODY>...</BODY><!--EndFragment--><HTML>
|
|
* are vailid too.
|
|
*/
|
|
public static byte[] convertToHTMLFormat(byte[] bytes) {
|
|
// Calculate section offsets
|
|
String htmlPrefix = "";
|
|
String htmlSuffix = "";
|
|
{
|
|
//we have extend the fragment to full HTML document correctly
|
|
//to avoid HTML and BODY tags doubling
|
|
String stContext = new String(bytes);
|
|
String stUpContext = stContext.toUpperCase();
|
|
if( -1 == stUpContext.indexOf("<HTML") ) {
|
|
htmlPrefix = "<HTML>";
|
|
htmlSuffix = "</HTML>";
|
|
if( -1 == stUpContext.indexOf("<BODY") ) {
|
|
htmlPrefix = htmlPrefix +"<BODY>";
|
|
htmlSuffix = "</BODY>" + htmlSuffix;
|
|
};
|
|
};
|
|
}
|
|
|
|
String stBaseUrl = DEF_SOURCE_URL;
|
|
int nStartHTML =
|
|
VERSION.length() + VERSION_NUM.length() + EOLN.length()
|
|
+ START_HTML.length() + PADDED_WIDTH + EOLN.length()
|
|
+ END_HTML.length() + PADDED_WIDTH + EOLN.length()
|
|
+ START_FRAGMENT.length() + PADDED_WIDTH + EOLN.length()
|
|
+ END_FRAGMENT.length() + PADDED_WIDTH + EOLN.length()
|
|
+ SOURCE_URL.length() + stBaseUrl.length() + EOLN.length()
|
|
;
|
|
int nStartFragment = nStartHTML + htmlPrefix.length();
|
|
int nEndFragment = nStartFragment + bytes.length - 1;
|
|
int nEndHTML = nEndFragment + htmlSuffix.length();
|
|
|
|
StringBuilder header = new StringBuilder(
|
|
nStartFragment
|
|
+ START_FRAGMENT_CMT.length()
|
|
);
|
|
//header
|
|
header.append(VERSION);
|
|
header.append(VERSION_NUM);
|
|
header.append(EOLN);
|
|
|
|
header.append(START_HTML);
|
|
header.append(toPaddedString(nStartHTML, PADDED_WIDTH));
|
|
header.append(EOLN);
|
|
|
|
header.append(END_HTML);
|
|
header.append(toPaddedString(nEndHTML, PADDED_WIDTH));
|
|
header.append(EOLN);
|
|
|
|
header.append(START_FRAGMENT);
|
|
header.append(toPaddedString(nStartFragment, PADDED_WIDTH));
|
|
header.append(EOLN);
|
|
|
|
header.append(END_FRAGMENT);
|
|
header.append(toPaddedString(nEndFragment, PADDED_WIDTH));
|
|
header.append(EOLN);
|
|
|
|
header.append(SOURCE_URL);
|
|
header.append(stBaseUrl);
|
|
header.append(EOLN);
|
|
|
|
//HTML
|
|
header.append(htmlPrefix);
|
|
|
|
byte[] headerBytes = null, trailerBytes = null;
|
|
|
|
try {
|
|
headerBytes = header.toString().getBytes(ENCODING);
|
|
trailerBytes = htmlSuffix.getBytes(ENCODING);
|
|
} catch (UnsupportedEncodingException cannotHappen) {
|
|
}
|
|
|
|
byte[] retval = new byte[headerBytes.length + bytes.length +
|
|
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);
|
|
retval[retval.length-1] = 0;
|
|
|
|
return retval;
|
|
}
|
|
|
|
////////////////////////////////////
|
|
//decoder instance data and methods:
|
|
|
|
private final BufferedInputStream bufferedStream;
|
|
private boolean descriptionParsed = false;
|
|
private boolean closed = false;
|
|
|
|
// 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";
|
|
|
|
//HTML header mapping:
|
|
private long iHTMLStart,// StartHTML -- shift in array to the first byte after the header
|
|
iHTMLEnd, // EndHTML -- shift in array of last byte for HTML syntax analysis
|
|
iFragStart,// StartFragment -- shift in array jast after <!--StartFragment-->
|
|
iFragEnd, // EndFragment -- shift in array before start <!--EndFragment-->
|
|
iSelStart, // StartSelection -- shift in array of the first char in copied selection
|
|
iSelEnd; // EndSelection -- shift in array of the last char in copied selection
|
|
private String stBaseURL; // SourceURL -- base URL for related referenses
|
|
private String stVersion; // Version -- current supported version
|
|
|
|
//Stream reader markers:
|
|
private long iStartOffset,
|
|
iEndOffset,
|
|
iReadCount;
|
|
|
|
private EHTMLReadMode readMode;
|
|
|
|
public HTMLCodec(
|
|
InputStream _bytestream,
|
|
EHTMLReadMode _readMode) throws IOException
|
|
{
|
|
bufferedStream = new BufferedInputStream(_bytestream, BYTE_BUFFER_LEN);
|
|
readMode = _readMode;
|
|
}
|
|
|
|
public synchronized String getBaseURL() throws IOException
|
|
{
|
|
if( !descriptionParsed ) {
|
|
parseDescription();
|
|
}
|
|
return stBaseURL;
|
|
}
|
|
public synchronized String getVersion() throws IOException
|
|
{
|
|
if( !descriptionParsed ) {
|
|
parseDescription();
|
|
}
|
|
return stVersion;
|
|
}
|
|
|
|
/**
|
|
* parseDescription parsing HTML clipboard header as it described in
|
|
* comment to convertToHTMLFormat
|
|
*/
|
|
private void parseDescription() throws IOException
|
|
{
|
|
stBaseURL = null;
|
|
stVersion = null;
|
|
|
|
// initialization of array offset pointers
|
|
// to the same "uninitialized" state.
|
|
iHTMLEnd =
|
|
iHTMLStart =
|
|
iFragEnd =
|
|
iFragStart =
|
|
iSelEnd =
|
|
iSelStart = -1;
|
|
|
|
bufferedStream.mark(BYTE_BUFFER_LEN);
|
|
String astEntries[] = new String[] {
|
|
//common
|
|
VERSION,
|
|
START_HTML,
|
|
END_HTML,
|
|
START_FRAGMENT,
|
|
END_FRAGMENT,
|
|
//ver 1.0
|
|
START_SELECTION,
|
|
END_SELECTION,
|
|
SOURCE_URL
|
|
};
|
|
BufferedReader bufferedReader = new BufferedReader(
|
|
new InputStreamReader(
|
|
bufferedStream,
|
|
ENCODING
|
|
),
|
|
CHAR_BUFFER_LEN
|
|
);
|
|
long iHeadSize = 0;
|
|
long iCRSize = EOLN.length();
|
|
int iEntCount = astEntries.length;
|
|
boolean bContinue = true;
|
|
|
|
for( int iEntry = 0; iEntry < iEntCount; ++iEntry ){
|
|
String stLine = bufferedReader.readLine();
|
|
if( null==stLine ) {
|
|
break;
|
|
}
|
|
//some header entries are optional, but the order is fixed.
|
|
for( ; iEntry < iEntCount; ++iEntry ){
|
|
if( !stLine.startsWith(astEntries[iEntry]) ) {
|
|
continue;
|
|
}
|
|
iHeadSize += stLine.length() + iCRSize;
|
|
String stValue = stLine.substring(astEntries[iEntry].length()).trim();
|
|
if( null!=stValue ) {
|
|
try{
|
|
switch( iEntry ){
|
|
case 0:
|
|
stVersion = stValue;
|
|
break;
|
|
case 1:
|
|
iHTMLStart = Integer.parseInt(stValue);
|
|
break;
|
|
case 2:
|
|
iHTMLEnd = Integer.parseInt(stValue);
|
|
break;
|
|
case 3:
|
|
iFragStart = Integer.parseInt(stValue);
|
|
break;
|
|
case 4:
|
|
iFragEnd = Integer.parseInt(stValue);
|
|
break;
|
|
case 5:
|
|
iSelStart = Integer.parseInt(stValue);
|
|
break;
|
|
case 6:
|
|
iSelEnd = Integer.parseInt(stValue);
|
|
break;
|
|
case 7:
|
|
stBaseURL = stValue;
|
|
break;
|
|
};
|
|
} catch ( NumberFormatException e ) {
|
|
throw new IOException(FAILURE_MSG + astEntries[iEntry]+ " value " + e + INVALID_MSG);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
//some entries could absent in HTML header,
|
|
//so we have find they by another way.
|
|
if( -1 == iHTMLStart )
|
|
iHTMLStart = iHeadSize;
|
|
if( -1 == iFragStart )
|
|
iFragStart = iHTMLStart;
|
|
if( -1 == iFragEnd )
|
|
iFragEnd = iHTMLEnd;
|
|
if( -1 == iSelStart )
|
|
iSelStart = iFragStart;
|
|
if( -1 == iSelEnd )
|
|
iSelEnd = iFragEnd;
|
|
|
|
//one of possible modes
|
|
switch( readMode ){
|
|
case HTML_READ_ALL:
|
|
iStartOffset = iHTMLStart;
|
|
iEndOffset = iHTMLEnd;
|
|
break;
|
|
case HTML_READ_FRAGMENT:
|
|
iStartOffset = iFragStart;
|
|
iEndOffset = iFragEnd;
|
|
break;
|
|
case HTML_READ_SELECTION:
|
|
default:
|
|
iStartOffset = iSelStart;
|
|
iEndOffset = iSelEnd;
|
|
break;
|
|
}
|
|
|
|
bufferedStream.reset();
|
|
if( -1 == iStartOffset ){
|
|
throw new IOException(FAILURE_MSG + "invalid HTML format.");
|
|
}
|
|
|
|
int curOffset = 0;
|
|
while (curOffset < iStartOffset){
|
|
curOffset += bufferedStream.skip(iStartOffset - curOffset);
|
|
}
|
|
|
|
iReadCount = curOffset;
|
|
|
|
if( iStartOffset != iReadCount ){
|
|
throw new IOException(FAILURE_MSG + "Byte stream ends in description.");
|
|
}
|
|
descriptionParsed = true;
|
|
}
|
|
|
|
@Override
|
|
public synchronized int read() throws IOException {
|
|
if( closed ){
|
|
throw new IOException("Stream closed");
|
|
}
|
|
|
|
if( !descriptionParsed ){
|
|
parseDescription();
|
|
}
|
|
if( -1 != iEndOffset && iReadCount >= iEndOffset ) {
|
|
return -1;
|
|
}
|
|
|
|
int retval = bufferedStream.read();
|
|
if( retval == -1 ) {
|
|
return -1;
|
|
}
|
|
++iReadCount;
|
|
return retval;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void close() throws IOException {
|
|
if( !closed ){
|
|
closed = true;
|
|
bufferedStream.close();
|
|
}
|
|
}
|
|
}
|