com.helger.commons.io.stream.StreamHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-commons Show documentation
Show all versions of ph-commons Show documentation
Java 1.8+ Library with tons of utility classes required in all projects
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.commons.io.stream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.LongConsumer;
import java.util.function.ObjIntConsumer;
import javax.annotation.CheckForSigned;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.CGlobal;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.builder.IBuilder;
import com.helger.commons.callback.exception.IExceptionCallback;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.exception.mock.IMockException;
import com.helger.commons.io.IHasInputStream;
import com.helger.commons.mutable.MutableLong;
import com.helger.commons.state.ESuccess;
import com.helger.commons.statistics.IMutableStatisticsHandlerSize;
import com.helger.commons.statistics.StatisticsManager;
/**
* Some very basic IO stream utility stuff. All input stream (=reading) related
* stuff is quite null
aware, where on writing an output stream may
* never be null.
*
* @author Philip Helger
*/
@Immutable
public final class StreamHelper
{
/** buffer size for copy operations */
public static final int DEFAULT_BUFSIZE = 16 * CGlobal.BYTES_PER_KILOBYTE;
/** The logger to use. */
private static final Logger LOGGER = LoggerFactory.getLogger (StreamHelper.class);
private static final IMutableStatisticsHandlerSize STATS_COPY_BYTES = StatisticsManager.getSizeHandler (StreamHelper.class.getName () +
"$COPY");
private static final IMutableStatisticsHandlerSize STATS_COPY_CHARS = StatisticsManager.getSizeHandler (StreamHelper.class.getName () +
"$COPYCHARS");
@PresentForCodeCoverage
private static final StreamHelper INSTANCE = new StreamHelper ();
private StreamHelper ()
{}
/**
* Check if the passed exception is a known EOF exception.
*
* @param t
* The throwable/exception to be checked. May be null
.
* @return true
if it is a user-created EOF exception
*/
public static boolean isKnownEOFException (@Nullable final Throwable t)
{
return t != null && isKnownEOFException (t.getClass ());
}
/**
* Check if the passed class is a known EOF exception class.
*
* @param aClass
* The class to be checked. May be null
.
* @return true
if it is a known EOF exception class.
*/
public static boolean isKnownEOFException (@Nullable final Class > aClass)
{
if (aClass == null)
return false;
final String sClass = aClass.getName ();
return sClass.equals ("java.io.EOFException") ||
sClass.equals ("org.mortbay.jetty.EofException") ||
sClass.equals ("org.eclipse.jetty.io.EofException") ||
sClass.equals ("org.apache.catalina.connector.ClientAbortException");
}
@Nullable
private static Exception _propagate (@Nonnull final Exception ex)
{
return ex instanceof IMockException ? null : ex;
}
/**
* Close the passed object, without trying to call flush on it.
*
* @param aCloseable
* The object to be closed. May be null
.
* @return {@link ESuccess#SUCCESS} if the object was successfully closed.
*/
@Nonnull
public static ESuccess closeWithoutFlush (@Nullable @WillClose final AutoCloseable aCloseable)
{
if (aCloseable != null)
{
try
{
// close stream
aCloseable.close ();
return ESuccess.SUCCESS;
}
catch (final Exception ex)
{
if (!isKnownEOFException (ex))
LOGGER.error ("Failed to close object " + aCloseable.getClass ().getName (), _propagate (ex));
}
}
return ESuccess.FAILURE;
}
/**
* Close the passed stream by encapsulating the declared {@link IOException}.
* If the passed object also implements the {@link Flushable} interface, it is
* tried to be flushed before it is closed.
*
* @param aCloseable
* The object to be closed. May be null
.
* @return {@link ESuccess} if the object was successfully closed.
*/
@Nonnull
public static ESuccess close (@Nullable @WillClose final AutoCloseable aCloseable)
{
if (aCloseable != null)
{
try
{
// flush object (if available)
if (aCloseable instanceof Flushable)
flush ((Flushable) aCloseable);
// close object
aCloseable.close ();
return ESuccess.SUCCESS;
}
catch (final NullPointerException ex)
{
// Happens if a java.io.FilterInputStream or java.io.FilterOutputStream
// has no underlying stream!
}
catch (final Exception ex)
{
if (!isKnownEOFException (ex))
LOGGER.error ("Failed to close object " + aCloseable.getClass ().getName (), _propagate (ex));
}
}
return ESuccess.FAILURE;
}
/**
* Flush the passed object encapsulating the declared {@link IOException}.
*
* @param aFlushable
* The flushable to be flushed. May be null
.
* @return {@link ESuccess#SUCCESS} if the object was successfully flushed.
*/
@Nonnull
public static ESuccess flush (@Nullable final Flushable aFlushable)
{
if (aFlushable != null)
try
{
aFlushable.flush ();
return ESuccess.SUCCESS;
}
catch (final NullPointerException ex)
{
// Happens if a java.io.FilterOutputStream is already closed!
}
catch (final IOException ex)
{
if (!isKnownEOFException (ex))
LOGGER.error ("Failed to flush object " + aFlushable.getClass ().getName (), _propagate (ex));
}
return ESuccess.FAILURE;
}
/**
* @return A newly created copy buffer using {@link #DEFAULT_BUFSIZE}. Never
* null
.
* @since 9.3.6
*/
@Nonnull
@ReturnsMutableCopy
public static byte [] createDefaultCopyBufferBytes ()
{
return new byte [DEFAULT_BUFSIZE];
}
/**
* Pass the content of the given input stream to the given output stream. The
* input stream is automatically closed, whereas the output stream stays open!
*
* @param aIS
* The input stream to read from. May be null
.
* Automatically closed!
* @param aOS
* The output stream to write to. May be null
. Not
* automatically closed!
* @return {@link ESuccess#SUCCESS}
if copying took place,
* {@link ESuccess#FAILURE}
otherwise
*/
@Nonnull
public static ESuccess copyInputStreamToOutputStream (@WillClose @Nullable final InputStream aIS,
@WillNotClose @Nullable final OutputStream aOS)
{
return copyByteStream ().from (aIS).closeFrom (true).to (aOS).closeTo (false).build ();
}
/**
* Pass the content of the given input stream to the given output stream. Both
* the input stream and the output stream are automatically closed.
*
* @param aIS
* The input stream to read from. May be null
.
* Automatically closed!
* @param aOS
* The output stream to write to. May be null
.
* Automatically closed!
* @return {@link ESuccess#SUCCESS}
if copying took place,
* {@link ESuccess#FAILURE}
otherwise
*/
@Nonnull
public static ESuccess copyInputStreamToOutputStreamAndCloseOS (@WillClose @Nullable final InputStream aIS,
@WillClose @Nullable final OutputStream aOS)
{
return copyByteStream ().from (aIS).closeFrom (true).to (aOS).closeTo (true).build ();
}
/**
* @return A new {@link CopyByteStreamBuilder}. Never null
.
*/
@Nonnull
public static CopyByteStreamBuilder copyByteStream ()
{
return new CopyByteStreamBuilder ();
}
/**
* A simple builder to copy an InputStream ({@link #from(InputStream)}) to an
* OutputStream ({@link #to(OutputStream)}) with certain parameters. Call
* {@link #build()} to execute the copying.
*
* @author Philip Helger
* @since 10.0.0
*/
public static class CopyByteStreamBuilder implements IBuilder
{
public static final boolean DEFAULT_CLOSE_SOURCE = false;
public static final boolean DEFAULT_CLOSE_DESTINATION = false;
private InputStream m_aIS;
private boolean m_bCloseIS = DEFAULT_CLOSE_SOURCE;
private OutputStream m_aOS;
private boolean m_bCloseOS = DEFAULT_CLOSE_DESTINATION;
private byte [] m_aBuffer;
private long m_nLimit = CGlobal.ILLEGAL_ULONG;
private IExceptionCallback m_aExceptionCallback;
private MutableLong m_aCopyByteCount;
private LongConsumer m_aProgressCallback;
/**
* @param a
* The InputStream to read from. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder from (@Nullable final InputStream a)
{
m_aIS = a;
return this;
}
/**
* @param b
* true
to close the InputStream, false
to
* leave it open. Default is {@link #DEFAULT_CLOSE_SOURCE}
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder closeFrom (final boolean b)
{
m_bCloseIS = b;
return this;
}
/**
* @param a
* The OutputStream to write to. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder to (@Nullable final OutputStream a)
{
m_aOS = a;
return this;
}
/**
* @param b
* true
to close the OutputStream, false
to
* leave it open.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder closeTo (final boolean b)
{
m_bCloseOS = b;
return this;
}
/**
* @param a
* The buffer to use. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder buffer (@Nullable final byte [] a)
{
m_aBuffer = a;
return this;
}
/**
* @param n
* An optional maximum number of bytes to copied from the InputStream
* to the OutputStream. May be < 0 to indicate no limit, meaning
* all bytes are copied.
* @return this for chaining
* @see #unlimited()
*/
@Nonnull
public CopyByteStreamBuilder limit (final long n)
{
m_nLimit = n;
return this;
}
/**
* @param a
* An optional maximum number of bytes to copied from the InputStream
* to the OutputStream. May be < 0 to indicate no limit, meaning
* all bytes are copied. If null
no limit is set
* @return this for chaining
* @since 10.1.0
* @see #unlimited()
*/
@Nonnull
public CopyByteStreamBuilder limit (@Nullable final Long a)
{
return a == null ? unlimited () : limit (a.longValue ());
}
/**
* Ensure no limit in copying (which is also the default).
*
* @return this for chaining
* @see #limit(long)
*/
@Nonnull
public CopyByteStreamBuilder unlimited ()
{
return limit (CGlobal.ILLEGAL_ULONG);
}
/**
* @param a
* The Exception callback to be invoked, if an exception occurs. May
* be null
.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder exceptionCallback (@Nullable final IExceptionCallback a)
{
m_aExceptionCallback = a;
return this;
}
/**
* @param a
* An optional mutable long object that will receive the total number
* of copied bytes. Note: and optional old value is overwritten.
* Note: this is only called, if copying was successful, and not in
* case of an exception.
* @return this for chaining
*/
@Nonnull
public CopyByteStreamBuilder copyByteCount (@Nullable final MutableLong a)
{
m_aCopyByteCount = a;
return this;
}
/**
* @param a
* An optional progress callback that takes the number of total bytes
* written during the copy action. It is first invoked after some
* byte were written.
* @return this for chaining
* @since 11.0.3
*/
@Nonnull
public CopyByteStreamBuilder progressCallback (@Nullable final LongConsumer a)
{
m_aProgressCallback = a;
return this;
}
@Nonnegative
private static long _copyInputStreamToOutputStream (@Nonnull @WillNotClose final InputStream aIS,
@Nonnull @WillNotClose final OutputStream aOS,
@Nonnull final byte [] aBuffer,
@Nullable final LongConsumer aProgressCallback) throws IOException
{
final int nBufferLength = aBuffer.length;
long nTotalBytesWritten = 0;
int nBytesRead;
// Potentially blocking read
while ((nBytesRead = aIS.read (aBuffer, 0, nBufferLength)) > -1)
{
if (nBytesRead > 0)
{
aOS.write (aBuffer, 0, nBytesRead);
nTotalBytesWritten += nBytesRead;
if (aProgressCallback != null)
aProgressCallback.accept (nTotalBytesWritten);
}
}
return nTotalBytesWritten;
}
@Nonnegative
private static long _copyInputStreamToOutputStreamWithLimit (@Nonnull @WillNotClose final InputStream aIS,
@Nonnull @WillNotClose final OutputStream aOS,
@Nonnull final byte [] aBuffer,
@Nonnegative final long nLimit,
@Nullable final LongConsumer aProgressCallback) throws IOException
{
final int nBufferLength = aBuffer.length;
long nRest = nLimit;
long nTotalBytesWritten = 0;
while (true)
{
// if nRest is smaller than aBuffer.length, which is an int, it is safe
// to
// cast nRest also to an int!
final int nBytesToRead = nRest >= nBufferLength ? nBufferLength : (int) nRest;
if (nBytesToRead == 0)
break;
// Potentially blocking read
final int nBytesRead = aIS.read (aBuffer, 0, nBytesToRead);
if (nBytesRead == -1)
{
// EOF
break;
}
if (nBytesRead > 0)
{
// At least one byte read
aOS.write (aBuffer, 0, nBytesRead);
nTotalBytesWritten += nBytesRead;
nRest -= nBytesRead;
if (aProgressCallback != null)
aProgressCallback.accept (nTotalBytesWritten);
}
}
return nTotalBytesWritten;
}
/**
* This method performs the main copying
*/
@Nonnull
public ESuccess build ()
{
try
{
if (m_aIS == null)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("The source InputStream is not set - hence no copying is possible");
return ESuccess.FAILURE;
}
if (m_aOS == null)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("The target OutputStream is not set - hence no copying is possible");
return ESuccess.FAILURE;
}
final byte [] aBuffer = m_aBuffer != null && m_aBuffer.length > 0 ? m_aBuffer : createDefaultCopyBufferBytes ();
// both streams are not null
final long nTotalBytesCopied;
if (m_nLimit < 0)
nTotalBytesCopied = _copyInputStreamToOutputStream (m_aIS, m_aOS, aBuffer, m_aProgressCallback);
else
nTotalBytesCopied = _copyInputStreamToOutputStreamWithLimit (m_aIS,
m_aOS,
aBuffer,
m_nLimit,
m_aProgressCallback);
// Add to statistics
STATS_COPY_BYTES.addSize (nTotalBytesCopied);
// Remember copied bytes?
if (m_aCopyByteCount != null)
m_aCopyByteCount.set (nTotalBytesCopied);
return ESuccess.SUCCESS;
}
catch (final IOException ex)
{
if (m_aExceptionCallback != null)
m_aExceptionCallback.onException (ex);
else
if (!isKnownEOFException (ex))
LOGGER.error ("Failed to copy from InputStream to OutputStream", _propagate (ex));
}
finally
{
// Ensure streams are closed under all circumstances
if (m_bCloseIS)
close (m_aIS);
if (m_bCloseOS)
close (m_aOS);
}
return ESuccess.FAILURE;
}
}
/**
* Get the number of available bytes in the passed input stream.
*
* @param aIS
* The input stream to use. May be null
.
* @return 0 in case of an error or if the parameter was null
.
*/
public static int getAvailable (@Nullable final InputStream aIS)
{
if (aIS != null)
try
{
return aIS.available ();
}
catch (final IOException ex)
{
// Fall through
}
return 0;
}
/**
* Get a byte buffer with all the available content of the passed input
* stream.
*
* @param aIS
* The source input stream. May not be null
.
* @return A new {@link NonBlockingByteArrayOutputStream} with all available
* content inside. The {@link OutputStream} must be closed by the
* caller since v10. Since v9.3.6 this method returns
* null
if copying fails.
*/
@Nullable
public static NonBlockingByteArrayOutputStream getCopy (@Nonnull @WillClose final InputStream aIS)
{
final int nAvailable = Math.max (DEFAULT_BUFSIZE, getAvailable (aIS));
final NonBlockingByteArrayOutputStream aBAOS = new NonBlockingByteArrayOutputStream (nAvailable);
if (copyByteStream ().from (aIS).closeFrom (true).to (aBAOS).closeTo (false).build ().isFailure ())
return null;
return aBAOS;
}
/**
* Get a byte buffer with all the available content of the passed input
* stream.
*
* @param aIS
* The source input stream. May not be null
.
* @param nLimit
* The maximum number of bytes to be copied to the output stream. Must
* be ≥ 0.
* @return A new {@link NonBlockingByteArrayOutputStream} with all available
* content inside. The {@link OutputStream} must be closed by the
* caller since v10. Since v9.3.6 this method returns
* null
if copying fails.
*/
@Nullable
public static NonBlockingByteArrayOutputStream getCopyWithLimit (@Nonnull @WillClose final InputStream aIS,
@Nonnegative final long nLimit)
{
final int nAvailable = Math.max (DEFAULT_BUFSIZE, getAvailable (aIS));
final NonBlockingByteArrayOutputStream aBAOS = new NonBlockingByteArrayOutputStream (nAvailable);
if (copyByteStream ().from (aIS).closeFrom (true).to (aBAOS).closeTo (false).limit (nLimit).build ().isFailure ())
return null;
return aBAOS;
}
/**
* Read all bytes from the passed input stream into a byte array.
*
* @param aISP
* The input stream provider to read from. May be null
.
* @return The byte array or null
if the parameter or the
* resolved input stream is null
.
*/
@Nullable
public static byte [] getAllBytes (@Nullable final IHasInputStream aISP)
{
if (aISP == null)
return null;
return getAllBytes (aISP.getInputStream ());
}
/**
* Read all bytes from the passed input stream into a byte array.
*
* @param aIS
* The input stream to read from. May be null
.
* @return The byte array or null
if the input stream is
* null
.
*/
@Nullable
public static byte [] getAllBytes (@Nullable @WillClose final InputStream aIS)
{
if (aIS == null)
return null;
try (final NonBlockingByteArrayOutputStream aBAOS = getCopy (aIS))
{
if (aBAOS == null)
return null;
// No need to copy, because the BAOS goes out of scope anyway
return aBAOS.getBufferOrCopy ();
}
}
/**
* Read all bytes from the passed input stream into a string.
*
* @param aISP
* The input stream provider to read from. May be null
.
* @param aCharset
* The charset to use. May not be null
.
* @return The String or null
if the parameter or the resolved
* input stream is null
.
*/
@Nullable
public static String getAllBytesAsString (@Nullable final IHasInputStream aISP,
@Nonnull @Nonempty final Charset aCharset)
{
if (aISP == null)
return null;
return getAllBytesAsString (aISP.getInputStream (), aCharset);
}
/**
* Read all bytes from the passed input stream into a string.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The charset to use. May not be null
.
* @return The String or null
if the input stream is
* null
.
*/
@Nullable
public static String getAllBytesAsString (@Nullable @WillClose final InputStream aIS,
@Nonnull @Nonempty final Charset aCharset)
{
ValueEnforcer.notNull (aCharset, "Charset");
if (aIS == null)
return null;
try (final NonBlockingByteArrayOutputStream aBAOS = getCopy (aIS))
{
if (aBAOS == null)
return null;
return aBAOS.getAsString (aCharset);
}
}
/**
* @return A newly created copy buffer using {@link #DEFAULT_BUFSIZE}. Never
* null
.
* @since 9.3.6
*/
@Nonnull
@ReturnsMutableCopy
public static char [] createDefaultCopyBufferChars ()
{
return new char [DEFAULT_BUFSIZE];
}
/**
* Pass the content of the given reader to the given writer. The reader is
* automatically closed, whereas the writer stays open!
*
* @param aReader
* The reader to read from. May be null
. Automatically
* closed!
* @param aWriter
* The writer to write to. May be null
. Not automatically
* closed!
* @return {@link ESuccess#SUCCESS}
if copying took place,
* {@link ESuccess#FAILURE}
otherwise
*/
@Nonnull
public static ESuccess copyReaderToWriter (@WillClose @Nullable final Reader aReader,
@WillNotClose @Nullable final Writer aWriter)
{
return copyCharStream ().from (aReader).closeFrom (true).to (aWriter).closeTo (false).build ();
}
/**
* Pass the content of the given reader to the given writer. The reader and
* the writer are automatically closed!
*
* @param aReader
* The reader to read from. May be null
. Automatically
* closed!
* @param aWriter
* The writer to write to. May be null
. Automatically
* closed!
* @return {@link ESuccess#SUCCESS}
if copying took place,
* {@link ESuccess#FAILURE}
otherwise
*/
@Nonnull
public static ESuccess copyReaderToWriterAndCloseWriter (@Nullable @WillClose final Reader aReader,
@Nullable @WillClose final Writer aWriter)
{
return copyCharStream ().from (aReader).closeFrom (true).to (aWriter).closeTo (true).build ();
}
/**
* @return A new {@link CopyCharStreamBuilder}. Never null
.
*/
@Nonnull
public static CopyCharStreamBuilder copyCharStream ()
{
return new CopyCharStreamBuilder ();
}
/**
* A simple builder to copy a Reader ({@link #from(Reader)}) to an Writer
* ({@link #to(Writer)}) with certain parameters. Call {@link #build()} to
* execute the copying.
*
* @author Philip Helger
* @since 10.0.0
*/
public static class CopyCharStreamBuilder implements IBuilder
{
public static final boolean DEFAULT_CLOSE_FROM = false;
public static final boolean DEFAULT_CLOSE_TO = false;
private Reader m_aReader;
private boolean m_bCloseReader = DEFAULT_CLOSE_FROM;
private Writer m_aWriter;
private boolean m_bCloseWriter = DEFAULT_CLOSE_TO;
private char [] m_aBuffer;
private long m_nLimit = CGlobal.ILLEGAL_ULONG;
private IExceptionCallback m_aExceptionCallback;
private MutableLong m_aCopyCharCount;
private LongConsumer m_aProgressCallback;
/**
* @param a
* The Reader to read from. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder from (@Nullable final Reader a)
{
m_aReader = a;
return this;
}
/**
* @param b
* true
to close the Reader, false
to leave
* it open. Default is {@link #DEFAULT_CLOSE_FROM}
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder closeFrom (final boolean b)
{
m_bCloseReader = b;
return this;
}
/**
* @param a
* The Writer to write to. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder to (@Nullable final Writer a)
{
m_aWriter = a;
return this;
}
/**
* @param b
* true
to close the Writer, false
to leave
* it open.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder closeTo (final boolean b)
{
m_bCloseWriter = b;
return this;
}
/**
* @param a
* The buffer to use. May be null
.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder buffer (@Nullable final char [] a)
{
m_aBuffer = a;
return this;
}
/**
* @param n
* An optional maximum number of chars to copied from the Reader to
* the Writer. May be < 0 to indicate no limit, meaning all chars
* are copied.
* @return this for chaining
* @see #unlimited()
*/
@Nonnull
public CopyCharStreamBuilder limit (final long n)
{
m_nLimit = n;
return this;
}
/**
* @param a
* An optional maximum number of chars to copied from the InputStream
* to the OutputStream. May be < 0 to indicate no limit, meaning
* all bytes are copied. If null
no limit is set
* @return this for chaining
* @since 10.1.0
* @see #unlimited()
*/
@Nonnull
public CopyCharStreamBuilder limit (@Nullable final Long a)
{
return a == null ? unlimited () : limit (a.longValue ());
}
/**
* Ensure no limit in copying (which is also the default).
*
* @return this for chaining
* @see #limit(long)
*/
@Nonnull
public CopyCharStreamBuilder unlimited ()
{
return limit (CGlobal.ILLEGAL_ULONG);
}
/**
* @param a
* The Exception callback to be invoked, if an exception occurs. May
* be null
.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder exceptionCallback (@Nullable final IExceptionCallback a)
{
m_aExceptionCallback = a;
return this;
}
/**
* @param a
* An optional mutable long object that will receive the total number
* of copied chars. Note: and optional old value is overwritten.
* Note: this is only called, if copying was successful, and not in
* case of an exception.
* @return this for chaining
*/
@Nonnull
public CopyCharStreamBuilder copyCharCount (@Nullable final MutableLong a)
{
m_aCopyCharCount = a;
return this;
}
/**
* @param a
* An optional progress callback that takes the number of total chars
* written during the copy action. It is first invoked after some
* chars were written.
* @return this for chaining
* @since 11.0.3
*/
@Nonnull
public CopyCharStreamBuilder progressCallback (@Nullable final LongConsumer a)
{
m_aProgressCallback = a;
return this;
}
@Nonnegative
private static long _copyReaderToWriter (@Nonnull @WillNotClose final Reader aReader,
@Nonnull @WillNotClose final Writer aWriter,
@Nonnull final char [] aBuffer,
@Nullable final LongConsumer aProgressCallback) throws IOException
{
long nTotalCharsWritten = 0;
int nCharsRead;
// Potentially blocking read
while ((nCharsRead = aReader.read (aBuffer, 0, aBuffer.length)) > -1)
{
if (nCharsRead > 0)
{
aWriter.write (aBuffer, 0, nCharsRead);
nTotalCharsWritten += nCharsRead;
if (aProgressCallback != null)
aProgressCallback.accept (nTotalCharsWritten);
}
}
return nTotalCharsWritten;
}
@Nonnegative
private static long _copyReaderToWriterWithLimit (@Nonnull @WillNotClose final Reader aReader,
@Nonnull @WillNotClose final Writer aWriter,
@Nonnull final char [] aBuffer,
@Nonnegative final long nLimit,
@Nullable final LongConsumer aProgressCallback) throws IOException
{
long nRest = nLimit;
long nTotalCharsWritten = 0;
while (true)
{
// if nRest is smaller than aBuffer.length, which is an int, it is safe
// to
// cast nRest also to an int!
final int nCharsToRead = nRest >= aBuffer.length ? aBuffer.length : (int) nRest;
if (nCharsToRead == 0)
break;
// Potentially blocking read
final int nCharsRead = aReader.read (aBuffer, 0, nCharsToRead);
if (nCharsRead == -1)
{
// EOF
break;
}
if (nCharsRead > 0)
{
// At least one byte read
aWriter.write (aBuffer, 0, nCharsRead);
nTotalCharsWritten += nCharsRead;
nRest -= nCharsRead;
if (aProgressCallback != null)
aProgressCallback.accept (nTotalCharsWritten);
}
}
return nTotalCharsWritten;
}
/**
* This method performs the main copying
*/
@Nonnull
public ESuccess build ()
{
try
{
if (m_aReader == null)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("The source Reader is not set - hence no copying is possible");
return ESuccess.FAILURE;
}
if (m_aWriter == null)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("The target Writer is not set - hence no copying is possible");
return ESuccess.FAILURE;
}
final char [] aBuffer = m_aBuffer != null && m_aBuffer.length > 0 ? m_aBuffer : createDefaultCopyBufferChars ();
// both streams are not null
final long nTotalCharsCopied;
if (m_nLimit < 0)
nTotalCharsCopied = _copyReaderToWriter (m_aReader, m_aWriter, aBuffer, m_aProgressCallback);
else
nTotalCharsCopied = _copyReaderToWriterWithLimit (m_aReader,
m_aWriter,
aBuffer,
m_nLimit,
m_aProgressCallback);
// Add to statistics
STATS_COPY_CHARS.addSize (nTotalCharsCopied);
// Remember copied bytes?
if (m_aCopyCharCount != null)
m_aCopyCharCount.set (nTotalCharsCopied);
return ESuccess.SUCCESS;
}
catch (final IOException ex)
{
if (m_aExceptionCallback != null)
m_aExceptionCallback.onException (ex);
else
if (!isKnownEOFException (ex))
LOGGER.error ("Failed to copy from Reader to Writer", _propagate (ex));
}
finally
{
// Ensure reader/writer are closed under all circumstances
if (m_bCloseReader)
close (m_aReader);
if (m_bCloseWriter)
close (m_aWriter);
}
return ESuccess.FAILURE;
}
}
@Nullable
public static NonBlockingStringWriter getCopy (@Nonnull @WillClose final Reader aReader)
{
final NonBlockingStringWriter aWriter = new NonBlockingStringWriter (DEFAULT_BUFSIZE);
if (copyCharStream ().from (aReader).closeFrom (true).to (aWriter).closeTo (false).build ().isFailure ())
return null;
return aWriter;
}
@Nullable
public static NonBlockingStringWriter getCopyWithLimit (@Nonnull @WillClose final Reader aReader,
@Nonnegative final long nLimit)
{
final NonBlockingStringWriter aWriter = new NonBlockingStringWriter (DEFAULT_BUFSIZE);
if (copyCharStream ().from (aReader)
.closeFrom (true)
.to (aWriter)
.closeTo (false)
.limit (nLimit)
.build ()
.isFailure ())
return null;
return aWriter;
}
/**
* Read all characters from the passed reader into a char array.
*
* @param aReader
* The reader to read from. May be null
.
* @return The character array or null
if the reader is
* null
.
*/
@Nullable
public static char [] getAllCharacters (@Nullable @WillClose final Reader aReader)
{
if (aReader == null)
return null;
try (final NonBlockingStringWriter aWriter = getCopy (aReader))
{
if (aWriter == null)
return null;
return aWriter.getAsCharArray ();
}
}
/**
* Read all characters from the passed reader into a String.
*
* @param aReader
* The reader to read from. May be null
.
* @return The character array or null
if the reader is
* null
.
*/
@Nullable
public static String getAllCharactersAsString (@Nullable @WillClose final Reader aReader)
{
if (aReader == null)
return null;
try (final NonBlockingStringWriter aWriter = getCopy (aReader))
{
if (aWriter == null)
return null;
return aWriter.getAsString ();
}
}
/**
* Get the content of the passed Spring resource as one big string in the
* passed character set.
*
* @param aISP
* The resource to read. May not be null
.
* @param aCharset
* The character set to use. May not be null
.
* @return null
if the resolved input stream is null
* , the content otherwise.
*/
@Nullable
@ReturnsMutableCopy
public static ICommonsList readStreamLines (@Nullable final IHasInputStream aISP,
@Nonnull final Charset aCharset)
{
return readStreamLines (aISP, aCharset, 0, CGlobal.ILLEGAL_UINT);
}
/**
* Get the content of the passed Spring resource as one big string in the
* passed character set.
*
* @param aISP
* The resource to read. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @param nLinesToSkip
* The 0-based index of the first line to read. Pass in 0 to indicate
* to read everything.
* @param nLinesToRead
* The number of lines to read. Pass in {@link CGlobal#ILLEGAL_UINT} to
* indicate that all lines should be read. If the number passed here
* exceeds the number of lines in the file, nothing happens.
* @return null
if the resolved input stream is null
* , the content otherwise.
*/
@Nullable
@ReturnsMutableCopy
public static ICommonsList readStreamLines (@Nullable final IHasInputStream aISP,
@Nonnull final Charset aCharset,
@Nonnegative final int nLinesToSkip,
@CheckForSigned final int nLinesToRead)
{
if (aISP == null)
return null;
return readStreamLines (aISP.getInputStream (), aCharset, nLinesToSkip, nLinesToRead);
}
/**
* Get the content of the passed stream as a list of lines in the passed
* character set.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @return null
if the input stream is null
, the
* content lines otherwise.
*/
@Nullable
@ReturnsMutableCopy
public static ICommonsList readStreamLines (@WillClose @Nullable final InputStream aIS,
@Nonnull @Nonempty final Charset aCharset)
{
return readStreamLines (aIS, aCharset, 0, CGlobal.ILLEGAL_UINT);
}
/**
* Get the content of the passed stream as a list of lines in the passed
* character set.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @param aTargetList
* The list to be filled with the lines. May not be null
.
*/
public static void readStreamLines (@WillClose @Nullable final InputStream aIS,
@Nonnull final Charset aCharset,
@Nonnull final List aTargetList)
{
if (aIS != null)
readStreamLines (aIS, aCharset, 0, CGlobal.ILLEGAL_UINT, aTargetList::add);
}
/**
* Get the content of the passed stream as a list of lines in the passed
* character set.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @param nLinesToSkip
* The 0-based index of the first line to read. Pass in 0 to indicate
* to read everything.
* @param nLinesToRead
* The number of lines to read. Pass in {@link CGlobal#ILLEGAL_UINT} to
* indicate that all lines should be read. If the number passed here
* exceeds the number of lines in the file, nothing happens.
* @return null
if the input stream is null
, the
* content lines otherwise.
*/
@Nullable
@ReturnsMutableCopy
public static ICommonsList readStreamLines (@WillClose @Nullable final InputStream aIS,
@Nonnull final Charset aCharset,
@Nonnegative final int nLinesToSkip,
@CheckForSigned final int nLinesToRead)
{
if (aIS == null)
return null;
// Read stream and collect all read lines in a list
final ICommonsList ret = new CommonsArrayList <> ();
readStreamLines (aIS, aCharset, nLinesToSkip, nLinesToRead, ret::add);
return ret;
}
/**
* Read the complete content of the passed stream and pass each line
* separately to the passed callback.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @param aLineCallback
* The callback that is invoked for all read lines. Each passed line
* does NOT contain the line delimiter!
*/
public static void readStreamLines (@WillClose @Nullable final InputStream aIS,
@Nonnull @Nonempty final Charset aCharset,
@Nonnull final Consumer super String> aLineCallback)
{
if (aIS != null)
readStreamLines (aIS, aCharset, 0, CGlobal.ILLEGAL_UINT, aLineCallback);
}
private static void _readFromReader (final int nLinesToSkip,
final int nLinesToRead,
@Nonnull final Consumer super String> aLineCallback,
final boolean bReadAllLines,
@Nonnull final NonBlockingBufferedReader aBR) throws IOException
{
// Skip all requested lines
String sLine;
for (int i = 0; i < nLinesToSkip; ++i)
{
sLine = aBR.readLine ();
if (sLine == null)
break;
}
if (bReadAllLines)
{
// Read all lines
while (true)
{
sLine = aBR.readLine ();
if (sLine == null)
break;
aLineCallback.accept (sLine);
}
}
else
{
// Read only a certain amount of lines
int nRead = 0;
while (true)
{
sLine = aBR.readLine ();
if (sLine == null)
break;
aLineCallback.accept (sLine);
++nRead;
if (nRead >= nLinesToRead)
break;
}
}
}
/**
* Read the content of the passed stream line by line and invoking a callback
* on all matching lines.
*
* @param aIS
* The input stream to read from. May be null
.
* @param aCharset
* The character set to use. May not be null
.
* @param nLinesToSkip
* The 0-based index of the first line to read. Pass in 0 to indicate
* to read everything.
* @param nLinesToRead
* The number of lines to read. Pass in {@link CGlobal#ILLEGAL_UINT} to
* indicate that all lines should be read. If the number passed here
* exceeds the number of lines in the file, nothing happens.
* @param aLineCallback
* The callback that is invoked for all read lines. Each passed line
* does NOT contain the line delimiter! Note: it is not invoked for
* skipped lines!
*/
public static void readStreamLines (@WillClose @Nullable final InputStream aIS,
@Nonnull @Nonempty final Charset aCharset,
@Nonnegative final int nLinesToSkip,
final int nLinesToRead,
@Nonnull final Consumer super String> aLineCallback)
{
try
{
ValueEnforcer.notNull (aCharset, "Charset");
ValueEnforcer.isGE0 (nLinesToSkip, "LinesToSkip");
final boolean bReadAllLines = nLinesToRead == CGlobal.ILLEGAL_UINT;
ValueEnforcer.isTrue (bReadAllLines || nLinesToRead >= 0,
() -> "Line count may not be that negative: " + nLinesToRead);
ValueEnforcer.notNull (aLineCallback, "LineCallback");
// Start the action only if there is something to read
if (aIS != null)
if (bReadAllLines || nLinesToRead > 0)
{
try (final NonBlockingBufferedReader aBR = new NonBlockingBufferedReader (createReader (aIS, aCharset)))
{
// read with the passed charset
_readFromReader (nLinesToSkip, nLinesToRead, aLineCallback, bReadAllLines, aBR);
}
catch (final IOException ex)
{
LOGGER.error ("Failed to read from reader", _propagate (ex));
}
}
}
finally
{
// Close input stream anyway
close (aIS);
}
}
/**
* Write bytes to an {@link OutputStream}.
*
* @param aOS
* The output stream to write to. May not be null
. Is
* closed independent of error or success.
* @param aBuf
* The byte array from which is to be written. May not be
* null
.
* @param nOfs
* The 0-based index to the first byte in the array to be written. May
* not be < 0.
* @param nLen
* The non-negative amount of bytes to be written. May not be < 0.
* @return {@link ESuccess}
*/
@Nonnull
public static ESuccess writeStream (@WillClose @Nonnull final OutputStream aOS,
@Nonnull final byte [] aBuf,
@Nonnegative final int nOfs,
@Nonnegative final int nLen)
{
try
{
ValueEnforcer.notNull (aOS, "OutputStream");
ValueEnforcer.isArrayOfsLen (aBuf, nOfs, nLen);
aOS.write (aBuf, nOfs, nLen);
aOS.flush ();
return ESuccess.SUCCESS;
}
catch (final IOException ex)
{
LOGGER.error ("Failed to write to output stream", _propagate (ex));
return ESuccess.FAILURE;
}
finally
{
close (aOS);
}
}
/**
* Write bytes to an {@link OutputStream}.
*
* @param aOS
* The output stream to write to. May not be null
. Is
* closed independent of error or success.
* @param aBuf
* The byte array to be written. May not be null
.
* @return {@link ESuccess}
*/
@Nonnull
public static ESuccess writeStream (@WillClose @Nonnull final OutputStream aOS, @Nonnull final byte [] aBuf)
{
return writeStream (aOS, aBuf, 0, aBuf.length);
}
/**
* Write bytes to an {@link OutputStream}.
*
* @param aOS
* The output stream to write to. May not be null
. Is
* closed independent of error or success.
* @param sContent
* The string to be written. May not be null
.
* @param aCharset
* The charset to be used, to convert the String to a byte array.
* @return {@link ESuccess}
*/
@Nonnull
public static ESuccess writeStream (@WillClose @Nonnull final OutputStream aOS,
@Nonnull final String sContent,
@Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (sContent, "Content");
ValueEnforcer.notNull (aCharset, "Charset");
return writeStream (aOS, sContent.getBytes (aCharset));
}
@Nonnull
public static NonBlockingStringReader createReader (@Nonnull final String sText)
{
return new NonBlockingStringReader (sText);
}
@Nonnull
public static NonBlockingStringReader createReader (@Nonnull final char [] aChars)
{
return new NonBlockingStringReader (aChars);
}
@Nullable
public static InputStreamReader createReader (@Nullable final InputStream aIS, @Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aCharset, "Charset");
return aIS == null ? null : new InputStreamReader (aIS, aCharset);
}
@Nullable
public static OutputStreamWriter createWriter (@Nullable final OutputStream aOS, @Nonnull final Charset aCharset)
{
ValueEnforcer.notNull (aCharset, "Charset");
return aOS == null ? null : new OutputStreamWriter (aOS, aCharset);
}
/**
* Fully skip the passed amounts in the input stream. Only forward skipping is
* possible!
*
* @param aIS
* The input stream to skip in.
* @param nBytesToSkip
* The number of bytes to skip. Must be ≥ 0.
* @throws IOException
* In case something goes wrong internally
*/
public static void skipFully (@Nonnull final InputStream aIS, @Nonnegative final long nBytesToSkip) throws IOException
{
ValueEnforcer.notNull (aIS, "InputStream");
ValueEnforcer.isGE0 (nBytesToSkip, "BytesToSkip");
long nRemaining = nBytesToSkip;
while (nRemaining > 0)
{
// May only return a partial skip
final long nSkipped = aIS.skip (nRemaining);
if (nSkipped == 0)
{
// Check if we're at the end of the file or not
// -> blocking read!
if (aIS.read () == -1)
{
throw new EOFException ("Failed to skip a total of " +
nBytesToSkip +
" bytes on input stream. Only skipped " +
(nBytesToSkip - nRemaining) +
" bytes so far!");
}
nRemaining--;
}
else
{
// Skipped at least one char
nRemaining -= nSkipped;
}
}
}
/**
* Read the whole buffer from the input stream.
*
* @param aIS
* The input stream to read from. May not be null
.
* @param aBuffer
* The buffer to write to. May not be null
. Must be ≥
* than the content to be read.
* @return The number of read bytes
* @throws IOException
* In case reading fails
*/
@Nonnegative
public static int readFully (@Nonnull final InputStream aIS, @Nonnull final byte [] aBuffer) throws IOException
{
return readFully (aIS, aBuffer, 0, aBuffer.length);
}
/**
* Read the whole buffer from the input stream.
*
* @param aIS
* The input stream to read from. May not be null
.
* @param aBuffer
* The buffer to write to. May not be null
. Must be ≥
* than the content to be read.
* @param nOfs
* The offset into the destination buffer to use. May not be < 0.
* @param nLen
* The number of bytes to read into the destination buffer to use. May
* not be < 0.
* @return The number of read bytes
* @throws IOException
* In case reading fails
*/
@Nonnegative
public static int readFully (@Nonnull @WillNotClose final InputStream aIS,
@Nonnull final byte [] aBuffer,
@Nonnegative final int nOfs,
@Nonnegative final int nLen) throws IOException
{
ValueEnforcer.notNull (aIS, "InputStream");
ValueEnforcer.isArrayOfsLen (aBuffer, nOfs, nLen);
int nTotalBytesRead = 0;
while (nTotalBytesRead < nLen)
{
// Potentially blocking read
final int nBytesRead = aIS.read (aBuffer, nOfs + nTotalBytesRead, nLen - nTotalBytesRead);
if (nBytesRead < 0)
throw new EOFException ("Failed to read a total of " +
nLen +
" bytes from input stream. Only read " +
nTotalBytesRead +
" bytes so far.");
nTotalBytesRead += nBytesRead;
}
return nTotalBytesRead;
}
private static void _readUntilEOF (@Nonnull @WillNotClose final InputStream aIS,
@Nonnull final byte [] aBuffer,
@Nonnull final ObjIntConsumer super byte []> aConsumer) throws IOException
{
ValueEnforcer.notNull (aIS, "InputStream");
ValueEnforcer.notNull (aBuffer, "Buffer");
ValueEnforcer.notNull (aConsumer, "Consumer");
int nBytesRead;
// Potentially blocking read
while ((nBytesRead = aIS.read (aBuffer, 0, aBuffer.length)) > -1)
aConsumer.accept (aBuffer, nBytesRead);
}
public static void readUntilEOF (@Nonnull @WillClose final InputStream aIS,
@Nonnull final ObjIntConsumer super byte []> aConsumer) throws IOException
{
readUntilEOF (aIS, createDefaultCopyBufferBytes (), aConsumer);
}
public static void readUntilEOF (@Nonnull @WillClose final InputStream aIS,
@Nonnull final byte [] aBuffer,
@Nonnull final ObjIntConsumer super byte []> aConsumer) throws IOException
{
try
{
ValueEnforcer.notNull (aIS, "InputStream");
ValueEnforcer.notNull (aBuffer, "Buffer");
ValueEnforcer.notNull (aConsumer, "Consumer");
_readUntilEOF (aIS, aBuffer, aConsumer);
}
finally
{
close (aIS);
}
}
private static void _readUntilEOF (@Nonnull @WillNotClose final Reader aReader,
@Nonnull final char [] aBuffer,
@Nonnull final ObjIntConsumer super char []> aConsumer) throws IOException
{
ValueEnforcer.notNull (aReader, "Reader");
ValueEnforcer.notNull (aBuffer, "Buffer");
ValueEnforcer.notNull (aConsumer, "Consumer");
int nCharsRead;
// Potentially blocking read
while ((nCharsRead = aReader.read (aBuffer, 0, aBuffer.length)) > -1)
aConsumer.accept (aBuffer, nCharsRead);
}
public static void readUntilEOF (@Nonnull @WillClose final Reader aReader,
@Nonnull final ObjIntConsumer super char []> aConsumer) throws IOException
{
readUntilEOF (aReader, new char [DEFAULT_BUFSIZE], aConsumer);
}
public static void readUntilEOF (@Nonnull @WillClose final Reader aReader,
@Nonnull final char [] aBuffer,
@Nonnull final ObjIntConsumer super char []> aConsumer) throws IOException
{
try
{
ValueEnforcer.notNull (aReader, "Reader");
ValueEnforcer.notNull (aBuffer, "Buffer");
ValueEnforcer.notNull (aConsumer, "Consumer");
_readUntilEOF (aReader, aBuffer, aConsumer);
}
finally
{
close (aReader);
}
}
public static boolean isBuffered (@Nullable final InputStream aIS)
{
return aIS instanceof BufferedInputStream ||
aIS instanceof NonBlockingBufferedInputStream ||
aIS instanceof ByteArrayInputStream ||
aIS instanceof NonBlockingByteArrayInputStream ||
aIS instanceof ByteBufferInputStream ||
(aIS instanceof WrappedInputStream && isBuffered (((WrappedInputStream) aIS).getWrappedInputStream ()));
}
@Nullable
public static InputStream getBuffered (@Nullable final InputStream aIS)
{
return aIS == null || isBuffered (aIS) ? aIS : new NonBlockingBufferedInputStream (aIS);
}
public static boolean isBuffered (@Nullable final OutputStream aOS)
{
return aOS instanceof BufferedOutputStream ||
aOS instanceof NonBlockingBufferedOutputStream ||
aOS instanceof ByteArrayOutputStream ||
aOS instanceof NonBlockingByteArrayOutputStream ||
aOS instanceof ByteBufferOutputStream ||
(aOS instanceof WrappedOutputStream && isBuffered (((WrappedOutputStream) aOS).getWrappedOutputStream ()));
}
@Nullable
public static OutputStream getBuffered (@Nullable final OutputStream aOS)
{
return aOS == null || isBuffered (aOS) ? aOS : new NonBlockingBufferedOutputStream (aOS);
}
public static boolean isBuffered (@Nullable final Reader aReader)
{
return aReader instanceof BufferedReader ||
aReader instanceof NonBlockingBufferedReader ||
aReader instanceof StringReader ||
aReader instanceof NonBlockingStringReader ||
(aReader instanceof WrappedReader && isBuffered (((WrappedReader) aReader).getWrappedReader ()));
}
@Nullable
public static Reader getBuffered (@Nullable final Reader aReader)
{
return aReader == null || isBuffered (aReader) ? aReader : new NonBlockingBufferedReader (aReader);
}
public static boolean isBuffered (@Nullable final Writer aWriter)
{
return aWriter instanceof BufferedWriter ||
aWriter instanceof NonBlockingBufferedWriter ||
aWriter instanceof StringWriter ||
aWriter instanceof NonBlockingStringWriter ||
(aWriter instanceof WrappedWriter && isBuffered (((WrappedWriter) aWriter).getWrappedWriter ()));
}
@Nullable
public static Writer getBuffered (@Nullable final Writer aWriter)
{
return aWriter == null || isBuffered (aWriter) ? aWriter : new NonBlockingBufferedWriter (aWriter);
}
@Nullable
public static InputStream checkForInvalidFilterInputStream (@Nullable final InputStream aIS)
{
if (aIS != null)
try
{
/*
* This will fail if IS is a FilterInputStream with a null
* contained InputStream. This happens e.g. when a JAR URL with a
* directory that does not end with a slash is returned.
*/
aIS.markSupported ();
}
catch (final NullPointerException ex)
{
// An InputStream for a directory was retrieved - ignore
close (aIS);
return null;
}
return aIS;
}
public static final int END_OF_STRING_MARKER = 0xfffdfffd;
/**
* Because {@link DataOutputStream#writeUTF(String)} has a limit of 64KB this
* methods provides a similar solution but simply writing the bytes.
*
* @param aDO
* {@link DataOutput} to write to. May not be null
.
* @param sStr
* The string to be written. May be null
.
* @throws IOException
* on write error
* @see #readSafeUTF(DataInput)
*/
public static void writeSafeUTF (@Nonnull final DataOutput aDO, @Nullable final String sStr) throws IOException
{
ValueEnforcer.notNull (aDO, "DataOutput");
if (sStr == null)
{
// Only write a 0 byte
// Writing a length of 0 would mean that the differentiation between
// "null" and
// empty string would be lost.
aDO.writeByte (0);
}
else
{
// Non-null indicator; basically the version of layout how the data was
// written
aDO.writeByte (2);
final byte [] aUTF8Bytes = sStr.getBytes (StandardCharsets.UTF_8);
// Write number of bytes
aDO.writeInt (aUTF8Bytes.length);
// Write main bytes
aDO.write (aUTF8Bytes);
// Was added in layout version 2:
aDO.writeInt (END_OF_STRING_MARKER);
}
}
/**
* Because {@link DataOutputStream#writeUTF(String)} has a limit of 64KB this
* methods provides a similar solution for reading like
* {@link DataInputStream#readUTF()} but what was written in
* {@link #writeSafeUTF(DataOutput, String)}.
*
* @param aDI
* {@link DataInput} to read from. May not be null
.
* @return The read string. May be null
.
* @throws IOException
* on read error
* @see #writeSafeUTF(DataOutput, String)
*/
@Nullable
public static String readSafeUTF (@Nonnull final DataInput aDI) throws IOException
{
ValueEnforcer.notNull (aDI, "DataInput");
final int nLayout = aDI.readByte ();
final String ret;
switch (nLayout)
{
case 0:
{
// If the first byte has value "0" it means the whole String is simply
// null
ret = null;
break;
}
case 1:
{
// length in UTF-8 bytes followed by the main bytes
final int nLength = aDI.readInt ();
final byte [] aData = new byte [nLength];
aDI.readFully (aData);
ret = new String (aData, StandardCharsets.UTF_8);
break;
}
case 2:
{
// length in UTF-8 bytes followed by the main bytes, than the end of
// byte marker
final int nLength = aDI.readInt ();
final byte [] aData = new byte [nLength];
aDI.readFully (aData);
ret = new String (aData, StandardCharsets.UTF_8);
final int nEndOfString = aDI.readInt ();
if (nEndOfString != END_OF_STRING_MARKER)
throw new IOException ("Missing end of String marker");
break;
}
default:
throw new IOException ("Unsupported string layout version " + nLayout);
}
return ret;
}
}