/* * RHQ Management Platform * Copyright (C) 2005-2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.util.stream; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Provides some utilities to work on streams and some (de)serialization methods.. * * @author John Mazzitelli */ public class StreamUtil { /** * Logger */ private static final Log LOG = LogFactory.getLog(StreamUtil.class); /** * Private to prevent instantiation. */ private StreamUtil() { } /** * Replace characters having special meaning <em>inside</em> HTML tags with * their escaped equivalents, using character entities such as * <tt>'&'</tt>. * <P> * The escaped characters are : * <ul> * <li>< * <li>> * <li>" * <li>' * <li>\ * <li>& * </ul> * <P> * This method ensures that arbitrary text appearing inside a tag does not * "confuse" the tag. For example, <tt>HREF='Blah.do?Page=1&Sort=ASC'</tt> * does not comply with strict HTML because of the ampersand, and should be * changed to <tt>HREF='Blah.do?Page=1&Sort=ASC'</tt>. This is * commonly seen in building query strings. (In JSTL, the c:url tag performs * this task automatically.) * * forHTMLTag is copy-n-pasted from: http://www.javapractices.com/Topic96.cjp * used to be in our util.StringUtil, we should really use jakarta's <code>StringEscapeUtils.escapeHTML()</code> * method, however, at this time we do not want to pull in the entire * Commons Lang API dependency for just one method. * * @param aTagFragment * some HTML to be escaped * @return escaped HTML */ private static String forHTMLTag(String aTagFragment) { final StringBuffer result = new StringBuffer(); final StringCharacterIterator iterator = new StringCharacterIterator(aTagFragment); for (char character = iterator.current(); character != CharacterIterator.DONE; character = iterator.next()) { switch (character) { case '<': result.append("<"); break; case '>': result.append(">"); break; case '\"': result.append("""); break; case '\'': result.append("'"); break; case '\\': result.append("\"); break; case '&': result.append("&"); break; case '|': result.append("|"); break; case ',': result.append(","); break; default: // the char is not a special one add it to the result as is result.append(character); break; } } return result.toString(); } /** * Reads in the entire contents of the given input stream and returns the data in a byte array. Be careful - if the * stream has a lot of data, you run the risk of an <code>OutOfMemoryError</code>. * * @param stream the stream to read * * @return the stream's data * * @throws RuntimeException if an IO exception occurred while reading the stream */ public static byte[] slurp(InputStream stream) throws RuntimeException { if (stream == null) { throw new IllegalArgumentException("Input stream is null."); } ByteArrayOutputStream out = new ByteArrayOutputStream(); copy(stream, out, true); return out.toByteArray(); } /** * Equivalent of {@link #slurp(InputStream)} but using a reader instead of input stream. * * @param reader * @return * @throws RuntimeException */ public static String slurp(Reader reader) throws RuntimeException { if (reader == null) { throw new IllegalArgumentException("Reader is null."); } StringWriter wrt = new StringWriter(); copy(reader, wrt); return wrt.toString(); } /** * Copies data from the input stream to the output stream. Upon completion or on an exception, the streams will be * closed. * * @param input the originating stream that contains the data to be copied * @param output the destination stream where the data should be copied to * * @return the number of bytes copied from the input to the output stream * * @throws RuntimeException if failed to read or write the data */ public static long copy(InputStream input, OutputStream output) throws RuntimeException { return copy(input, output, true); } /** * Equivalent of {@link #copy(InputStream, OutputStream)} but using reader and writer instead * of streams. * * @param rdr * @param wrt * @return * @throws RuntimeException */ public static long copy(Reader rdr, Writer wrt) throws RuntimeException { return copy(rdr, wrt, true); } /** * Copies data from the input stream to the output stream. Upon completion or on an exception, the streams will be * closed but only if <code>closeStreams</code> is <code>true</code>. If <code>closeStreams</code> is <code> * false</code>, the streams are left open; the caller has the responsibility to close them. * * @param input the originating stream that contains the data to be copied * @param output the destination stream where the data should be copied to * @param closeStreams if <code>true</code>, the streams will be closed before the method returns * * @return the number of bytes copied from the input to the output stream * * @throws RuntimeException if failed to read or write the data */ public static long copy(InputStream input, OutputStream output, boolean closeStreams) throws RuntimeException { return copy(input, output, closeStreams, false); } /** * Copies data from the input stream to the output stream. Upon completion or on an exception, the streams will be * closed but only if <code>closeStreams</code> is <code>true</code>. If <code>closeStreams</code> is <code> * false</code>, the streams are left open; the caller has the reponsibility to close them. * <p> * If htmlEscape is <code>true</code> the input stream is read into a <code>String</code> and all HTML entities * are escaped using {@link #forHTMLTag(String)} prior to being copied to output stream. * * @param input the originating stream that contains the data to be copied * @param output the destination stream where the data should be copied to * @param closeStreams if <code>true</code>, the streams will be closed before the method returns * @param htmlEscape if <code>true</code>, the input stream will be HTML escaped before being written to output stream * * @return the number of bytes copied from the input to the output stream or the number of characters stored in output stream if htmlEscape is <code>true</code> * * @throws RuntimeException if failed to read or write the data * * @since 4.4 */ public static long copy(InputStream input, OutputStream output, boolean closeStreams, boolean htmlEscape) throws RuntimeException { if (input == null) { throw new IllegalArgumentException("Input stream is null."); } if (output == null) { throw new IllegalArgumentException("Output stream is null."); } long numBytesCopied = 0; int bufferSize = 32768; try { // make sure we buffer the input input = new BufferedInputStream(input, bufferSize); byte[] buffer = new byte[bufferSize]; for (int bytesRead = input.read(buffer); bytesRead != -1; bytesRead = input.read(buffer)) { if (htmlEscape) { String htmlEncodedStr = forHTMLTag(new String(buffer, 0, bytesRead)); bytesRead = htmlEncodedStr.length(); output.write(htmlEncodedStr.getBytes(), 0, bytesRead); } else { output.write(buffer, 0, bytesRead); } numBytesCopied += bytesRead; } output.flush(); } catch (IOException ioe) { throw new RuntimeException("Stream data cannot be copied", ioe); } finally { if (closeStreams) { try { output.close(); } catch (IOException ioe2) { LOG.warn("Streams could not be closed", ioe2); } try { input.close(); } catch (IOException ioe2) { LOG.warn("Streams could not be closed", ioe2); } } } return numBytesCopied; } /** * Equivalent of {@link #copy(InputStream, OutputStream, boolean)} only using reader and writer * instead of input stream and output stream. * * @param rdr * @param wrt * @param closeStreams * @return * @throws RuntimeException */ public static long copy(Reader rdr, Writer wrt, boolean closeStreams) throws RuntimeException { if (rdr == null) { throw new IllegalArgumentException("Reader is null."); } if (wrt == null) { throw new IllegalArgumentException("Writer is null."); } try { long numCharsCopied = 0; char[] buffer = new char[32768]; int cnt; while ((cnt = rdr.read(buffer)) != -1) { numCharsCopied += cnt; wrt.write(buffer, 0, cnt); } return numCharsCopied; } catch (IOException e) { throw new RuntimeException("Reader could not have been copied to the writer.", e); } finally { if (closeStreams) { try { rdr.close(); } catch (IOException ioe) { LOG.warn("Reader could not be closed.", ioe); } try { wrt.close(); } catch (IOException ioe) { LOG.warn("Writer could not be closed.", ioe); } } } } /** * Copies data from the input stream to the output stream. The caller has the responsibility to close them. This * method allows you to copy a byte range from the input stream. The start byte is the index (where the first byte * of the stream is index #0) that starts to be copied. <code>length</code> indicates how many bytes to copy, a * negative length indicates copy everything up to the EOF of the input stream. * * <p>Because this method must leave the given input stream intact in case the caller wants to continue reading from * the input stream (that is, in case the caller wants to read the next byte after the final byte read by this * method), this method will not wrap the input stream with a buffered input stream. Because of this, this method is * less efficient than {@link #copy(InputStream, OutputStream, boolean)}. If you do not care to continue reading * from the input stream after this method completes, it is recommended you wrap your input stream in a * {@link BufferedInputStream} and pass that buffered stream to this method.</p> * * @param input the originating stream that contains the data to be copied * @param output the destination stream where the data should be copied to * @param startByte the first byte to copy from the input stream (byte indexes start at #0) * @param length the number of bytes to copy - if -1, then copy all until EOF * * @return the number of bytes copied from the input to the output stream (usually length, but if length was larger * than the number of bytes in <code>input</code> after the start byte, this return value will be less than * <code>length</code>. * * @throws RuntimeException if failed to read or write the data */ public static long copy(InputStream input, OutputStream output, long startByte, long length) throws RuntimeException { if (input == null) { throw new IllegalArgumentException("Input stream is null."); } if (output == null) { throw new IllegalArgumentException("Output stream is null."); } if (length == 0) { return 0; } if (startByte < 0) { throw new IllegalArgumentException("startByte=" + startByte); } long numBytesCopied = 0; int bufferSize = 32768; try { byte[] buffer = new byte[bufferSize]; if (startByte > 0) { input.skip(startByte); // skips so the next read will read byte #startByte } // ok to cast to int, if length is less then bufferSize it must be able to fit into int int bytesRead = input.read(buffer, 0, ((length < 0) || (length >= bufferSize)) ? bufferSize : (int) length); while (bytesRead > 0) { output.write(buffer, 0, bytesRead); numBytesCopied += bytesRead; length -= bytesRead; bytesRead = input.read(buffer, 0, ((length < 0) || (length >= bufferSize)) ? bufferSize : (int) length); } output.flush(); } catch (IOException ioe) { throw new RuntimeException("Stream data cannot be copied", ioe); } return numBytesCopied; } /** * Given a serializable object, this will return the object's serialized byte array representation. * * @param object the object to serialize * * @return the serialized bytes * * @throws RuntimeException if failed to serialize the object */ public static byte[] serialize(Serializable object) throws RuntimeException { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream oos; try { oos = new ObjectOutputStream(byteStream); oos.writeObject(object); oos.close(); } catch (IOException ioe) { throw new RuntimeException("Failed to serialize object", ioe); } return byteStream.toByteArray(); } /** * Deserializes the given serialization data and returns the object. * * @param serializedData the serialized data as a byte array * * @return the deserialized object * * @throws RuntimeException if failed to deserialize the object */ public static Object deserialize(byte[] serializedData) throws RuntimeException { ByteArrayInputStream byteStream = new ByteArrayInputStream(serializedData); ObjectInputStream ois; Object retObject; try { ois = new ObjectInputStream(byteStream); retObject = ois.readObject(); ois.close(); } catch (Exception e) { throw new RuntimeException("Failed to deserialize object", e); } return retObject; } /** * Can be used to safely close a stream. No-op if the stream is null. * * @param stream the stream to close or null */ public static void safeClose(Closeable stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { LOG.error("Failed to close a stream.", e); } } } }