/* * Copyright (c) 2000, 2018, 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.nio.charset.Charset; 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 predefinedClipboardNameMap; static { Map 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 getFormatsForFlavors( DataFlavor[] flavors, FlavorTable map) { SortedMap 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 = Charset.defaultCharset().name(); 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 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 = ""; public static final String END_FRAGMENT_CMT = ""; 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 * EndFragment:000000694\r\n -- shift in array before start * 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 * .............................. * ^ ^ ^ ^^ ^ * \ StartHTML | \-StartSelection | \EndFragment EndHTML/ * \-StartFragment \EndSelection * *Combinations with tags sequence *...... * or *......... * 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(""; htmlSuffix = "" + 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 iFragEnd, // EndFragment -- shift in array before start 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(); } } }