You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2019/08/09 14:36:44 UTC

[commons-io] branch master updated: [IO-615] Add classes TeeWriter, FilterCollectionWriter, ProxyCollectionWriter, IOExceptionList, IOIndexedException.

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git


The following commit(s) were added to refs/heads/master by this push:
     new 1ee660d  [IO-615] Add classes TeeWriter, FilterCollectionWriter, ProxyCollectionWriter, IOExceptionList, IOIndexedException.
1ee660d is described below

commit 1ee660d9f1f2af298b7ebe49ddf8191592267aca
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Fri Aug 9 10:36:39 2019 -0400

    [IO-615] Add classes TeeWriter, FilterCollectionWriter,
    ProxyCollectionWriter, IOExceptionList, IOIndexedException.
    
    Closes #88.
---
 src/changes/changes.xml                            |   3 +
 .../org/apache/commons/io/IOExceptionList.java     |  89 ++++
 .../org/apache/commons/io/IOIndexedException.java  |  60 +++
 .../commons/io/output/FilterCollectionWriter.java  | 302 ++++++++++++++
 .../commons/io/output/ProxyCollectionWriter.java   | 280 +++++++++++++
 .../org/apache/commons/io/output/TeeWriter.java    |  51 +++
 .../apache/commons/io/IOExceptionListTestCase.java |  58 +++
 .../commons/io/IOIndexedExceptionTestCase.java     |  48 +++
 .../io/output/ProxyCollectionWriterTest.java       | 449 +++++++++++++++++++++
 .../apache/commons/io/output/TeeWriterTest.java    | 449 +++++++++++++++++++++
 10 files changed, 1789 insertions(+)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9d627c1..888aeb5 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -119,6 +119,9 @@ The <action> type attribute can be add,update,fix,remove.
       <action issue="IO-614" dev="ggregory" type="add" due-to="Rob Spoor">
         Add classes TaggedWriter, ClosedWriter and BrokenWriter. #86.
       </action>
+      <action issue="IO-615" dev="ggregory" type="add" due-to="Gary Gregory, Rob Spoor">
+        Add classes TeeWriter, FilterCollectionWriter, ProxyCollectionWriter, IOExceptionList, IOIndexedException.
+      </action>
     </release>
 
     <release version="2.6" date="2017-10-15" description="Java 7 required, Java 9 supported.">
diff --git a/src/main/java/org/apache/commons/io/IOExceptionList.java b/src/main/java/org/apache/commons/io/IOExceptionList.java
new file mode 100644
index 0000000..f013f68
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/IOExceptionList.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A IOException based on a list of Throwable causes.
+ * <p>
+ * The first exception in the list is used as this exception's cause and is accessible with the usual
+ * {@link #getCause()} while the complete list is accessible with {@link #getCauseList()}.
+ * </p>
+ *
+ * @since 2.7
+ */
+public class IOExceptionList extends IOException {
+
+    private static final long serialVersionUID = 1L;
+    private final List<? extends Throwable> causeList;
+
+    /**
+     * Creates a new exception caused by a list of exceptions.
+     *
+     * @param causeList a list of cause exceptions.
+     */
+    public IOExceptionList(List<? extends Throwable> causeList) {
+        super(String.format("%,d exceptions: %s", causeList == null ? 0 : causeList.size(), causeList),
+                causeList == null ? null : causeList.get(0));
+        this.causeList = causeList == null ? Collections.emptyList() : causeList;
+    }
+
+    /**
+     * Gets the cause list.
+     * 
+     * @return The list of causes.
+     */
+    public <T extends Throwable> List<T> getCauseList() {
+        return (List<T>) causeList;
+    }
+
+    /**
+     * Gets the cause list.
+     * 
+     * @param index index in the cause list.
+     * @return The list of causes.
+     */
+    public <T extends Throwable> T getCause(final int index) {
+        return (T) causeList.get(index);
+    }
+
+    /**
+     * Gets the cause list.
+     * 
+     * @param index index in the cause list.
+     * @return The list of causes.
+     */
+    public <T extends Throwable> T getCause(final int index, Class<T> clazz) {
+        return (T) causeList.get(index);
+    }
+
+    /**
+     * Works around Throwable and Generics, may fail at runtime depending on the argument value.
+     * 
+     * @param <T>   the target type
+     * @param clazz the target type
+     * @return The list of causes.
+     */
+    public <T extends Throwable> List<T> getCauseList(Class<T> clazz) {
+        return (List<T>) causeList;
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/io/IOIndexedException.java b/src/main/java/org/apache/commons/io/IOIndexedException.java
new file mode 100644
index 0000000..635c9ad
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/IOIndexedException.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io;
+
+import java.io.IOException;
+
+/**
+ * A IOException associated with a source index.
+ *
+ * @since 2.7
+ */
+public class IOIndexedException extends IOException {
+
+    private static final long serialVersionUID = 1L;
+    private final int index;
+
+    /**
+     * Creates a new exception.
+     *
+     * @param index index of this exception.
+     * @param cause cause exceptions.
+     */
+    public IOIndexedException(final int index, final Throwable cause) {
+        super(toMessage(index, cause), cause);
+        this.index = index;
+    }
+
+    protected static String toMessage(final int index, final Throwable cause) {
+        // Letting index be any int
+        final String unspecified = "Null";
+        final String name = cause == null ? unspecified : cause.getClass().getSimpleName();
+        final String msg = cause == null ? unspecified : cause.getMessage();
+        return String.format("%s #%,d: %s", name, index, msg);
+    }
+
+    /**
+     * The index of this exception.
+     *
+     * @return index of this exception.
+     */
+    public int getIndex() {
+        return index;
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java
new file mode 100644
index 0000000..ed9114c
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java
@@ -0,0 +1,302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io.output;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.io.IOExceptionList;
+import org.apache.commons.io.IOIndexedException;
+
+/**
+ * Abstract class for writing filtered character streams to a {@link Collection} of writers. This is in contrast to
+ * {@link FilterWriter} which is backed by a single {@link Writer}.
+ * <p>
+ * This abstract class provides default methods that pass all requests to the contained writers. Subclasses should
+ * likely override some of these methods.
+ * </p>
+ * <p>
+ * The class {@link Writer} defines method signatures with {@code throws} {@link IOException}, which in this class are
+ * actually {@link IOExceptionList} containing a list of {@link IOIndexedException}.
+ * </p>
+ * 
+ * @since 2.7
+ */
+public class FilterCollectionWriter extends Writer {
+
+    /**
+     * Empty and immutable collection of writers.
+     */
+    protected final Collection<Writer> EMPTY_WRITERS = Collections.emptyList();
+
+    /**
+     * The underlying writers.
+     */
+    protected final Collection<Writer> writers;
+
+    /**
+     * Creates a new filtered collection writer.
+     *
+     * @param writers Writers to provide the underlying targets.
+     */
+    protected FilterCollectionWriter(final Collection<Writer> writers) {
+        this.writers = writers == null ? EMPTY_WRITERS : writers;
+    }
+
+    /**
+     * Creates a new filtered collection writer.
+     *
+     * @param writers Writers to provide the underlying targets.
+     */
+    protected FilterCollectionWriter(final Writer... writers) {
+        this.writers = writers == null ? EMPTY_WRITERS : Arrays.asList(writers);
+    }
+
+    @Override
+    public Writer append(final char c) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.append(c);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+        return this;
+    }
+
+    @Override
+    public Writer append(final CharSequence csq) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.append(csq);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+        return this;
+    }
+
+    @Override
+    public Writer append(final CharSequence csq, final int start, final int end) throws IOException {
+
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.append(csq, start, end);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+        return this;
+    }
+
+    @Override
+    public void close() throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.close();
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+
+    }
+
+    /**
+     * Flushes the stream.
+     *
+     * @exception IOException If an I/O error occurs
+     */
+    @Override
+    public void flush() throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.flush();
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+
+    }
+
+    /**
+     * Writes a portion of an array of characters.
+     *
+     * @param cbuf Buffer of characters to be written
+     * @param off  Offset from which to start reading characters
+     * @param len  Number of characters to be written
+     *
+     * @exception IOException If an I/O error occurs
+     */
+    @Override
+    public void write(final char cbuf[], final int off, final int len) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.write(cbuf, off, len);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+    }
+
+    @Override
+    public void write(final char[] cbuf) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.write(cbuf);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+    }
+
+    /**
+     * Writes a single character.
+     *
+     * @exception IOException If an I/O error occurs
+     */
+    @Override
+    public void write(final int c) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.write(c);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+    }
+
+    @Override
+    public void write(final String str) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.write(str);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+
+    }
+
+    /**
+     * Writes a portion of a string.
+     *
+     * @param str String to be written
+     * @param off Offset from which to start reading characters
+     * @param len Number of characters to be written
+     *
+     * @exception IOException If an I/O error occurs
+     */
+    @Override
+    public void write(final String str, final int off, final int len) throws IOException {
+        final List<Exception> causeList = new ArrayList<>();
+        int i = 0;
+        for (final Writer w : writers) {
+            if (w != null) {
+                try {
+                    w.write(str, off, len);
+                } catch (final IOException e) {
+                    causeList.add(new IOIndexedException(i, e));
+                }
+            }
+            i++;
+        }
+        if (!causeList.isEmpty()) {
+            throw new IOExceptionList(causeList);
+        }
+
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java b/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java
new file mode 100644
index 0000000..b5fdcfb
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io.output;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * A Proxy stream collection which acts as expected, that is it passes the method calls on to the proxied streams and
+ * doesn't change which methods are being called. It is an alternative base class to {@link FilterWriter} and
+ * {@link FilterCollectionWriter} to increase reusability, because FilterWriter changes the methods being called, such
+ * as {@code write(char[])} to {@code write(char[], int, int)} and {@code write(String)} to
+ * {@code write(String, int, int)}. This is in contrast to {@link ProxyWriter} which is backed by a single
+ * {@link Writer}.
+ *
+ * @since 2.7
+ */
+public class ProxyCollectionWriter extends FilterCollectionWriter {
+
+    /**
+     * Creates a new proxy collection writer.
+     *
+     * @param writers Writers object to provide the underlying targets.
+     */
+    public ProxyCollectionWriter(final Collection<Writer> writers) {
+        super(writers);
+    }
+
+    /**
+     * Creates a new proxy collection writer.
+     *
+     * @param writers Writers to provide the underlying targets.
+     */
+    public ProxyCollectionWriter(final Writer... writers) {
+        super(writers);
+    }
+
+    /**
+     * Invoked by the write methods after the proxied call has returned successfully. The number of chars written (1 for
+     * the {@link #write(int)} method, buffer length for {@link #write(char[])}, etc.) is given as an argument.
+     * <p>
+     * Subclasses can override this method to add common post-processing functionality without having to override all
+     * the write methods. The default implementation does nothing.
+     * </p>
+     *
+     * @param n number of chars written
+     * @throws IOException if the post-processing fails
+     */
+    protected void afterWrite(final int n) throws IOException {
+        // noop
+    }
+
+    /**
+     * Invokes the delegates' <code>append(char)</code> methods.
+     *
+     * @param c The character to write
+     * @return this writer
+     * @throws IOException if an I/O error occurs
+     * @since 2.0
+     */
+    @Override
+    public Writer append(final char c) throws IOException {
+        try {
+            beforeWrite(1);
+            super.append(c);
+            afterWrite(1);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+        return this;
+    }
+
+    /**
+     * Invokes the delegates' <code>append(CharSequence)</code> methods.
+     *
+     * @param csq The character sequence to write
+     * @return this writer
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public Writer append(final CharSequence csq) throws IOException {
+        try {
+            final int len = IOUtils.length(csq);
+            beforeWrite(len);
+            super.append(csq);
+            afterWrite(len);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+        return this;
+    }
+
+    /**
+     * Invokes the delegates' <code>append(CharSequence, int, int)</code> methods.
+     *
+     * @param csq   The character sequence to write
+     * @param start The index of the first character to write
+     * @param end   The index of the first character to write (exclusive)
+     * @return this writer
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public Writer append(final CharSequence csq, final int start, final int end) throws IOException {
+        try {
+            beforeWrite(end - start);
+            super.append(csq, start, end);
+            afterWrite(end - start);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+        return this;
+    }
+
+    /**
+     * Invoked by the write methods before the call is proxied. The number of chars to be written (1 for the
+     * {@link #write(int)} method, buffer length for {@link #write(char[])}, etc.) is given as an argument.
+     * <p>
+     * Subclasses can override this method to add common pre-processing functionality without having to override all the
+     * write methods. The default implementation does nothing.
+     * </p>
+     *
+     * @param n number of chars to be written
+     * @throws IOException if the pre-processing fails
+     */
+    protected void beforeWrite(final int n) throws IOException {
+        // noop
+    }
+
+    /**
+     * Invokes the delegate's <code>close()</code> method.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        try {
+            super.close();
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>flush()</code> method.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void flush() throws IOException {
+        try {
+            super.flush();
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Handle any IOExceptions thrown.
+     * <p>
+     * This method provides a point to implement custom exception handling. The default behaviour is to re-throw the
+     * exception.
+     * </p>
+     *
+     * @param e The IOException thrown
+     * @throws IOException if an I/O error occurs
+     */
+    protected void handleIOException(final IOException e) throws IOException {
+        throw e;
+    }
+
+    /**
+     * Invokes the delegate's <code>write(char[])</code> method.
+     *
+     * @param cbuf the characters to write
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void write(final char[] cbuf) throws IOException {
+        try {
+            final int len = IOUtils.length(cbuf);
+            beforeWrite(len);
+            super.write(cbuf);
+            afterWrite(len);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>write(char[], int, int)</code> method.
+     *
+     * @param cbuf the characters to write
+     * @param off  The start offset
+     * @param len  The number of characters to write
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void write(final char[] cbuf, final int off, final int len) throws IOException {
+        try {
+            beforeWrite(len);
+            super.write(cbuf, off, len);
+            afterWrite(len);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>write(int)</code> method.
+     *
+     * @param c the character to write
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void write(final int c) throws IOException {
+        try {
+            beforeWrite(1);
+            super.write(c);
+            afterWrite(1);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>write(String)</code> method.
+     *
+     * @param str the string to write
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void write(final String str) throws IOException {
+        try {
+            final int len = IOUtils.length(str);
+            beforeWrite(len);
+            super.write(str);
+            afterWrite(len);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+    /**
+     * Invokes the delegate's <code>write(String)</code> method.
+     *
+     * @param str the string to write
+     * @param off The start offset
+     * @param len The number of characters to write
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void write(final String str, final int off, final int len) throws IOException {
+        try {
+            beforeWrite(len);
+            super.write(str, off, len);
+            afterWrite(len);
+        } catch (final IOException e) {
+            handleIOException(e);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/io/output/TeeWriter.java b/src/main/java/org/apache/commons/io/output/TeeWriter.java
new file mode 100644
index 0000000..3fc0168
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/output/TeeWriter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io.output;
+
+import java.io.Writer;
+import java.util.Collection;
+
+/**
+ * Classic splitter of {@link Writer}. Named after the Unix 'tee' command. It allows a stream to be branched off so
+ * there are now two streams.
+ * <p>
+ * This currently a only convenience class with the proper name "TeeWriter".
+ * </p>
+ * 
+ * @since 2.7
+ */
+public class TeeWriter extends ProxyCollectionWriter {
+
+    /**
+     * Creates a new filtered collection writer.
+     *
+     * @param writers Writers to provide the underlying targets.
+     */
+    public TeeWriter(final Collection<Writer> writers) {
+        super(writers);
+    }
+
+    /**
+     * Creates a new filtered collection writer.
+     *
+     * @param writers Writers to provide the underlying targets.
+     */
+    public TeeWriter(final Writer... writers) {
+        super(writers);
+    }
+}
diff --git a/src/test/java/org/apache/commons/io/IOExceptionListTestCase.java b/src/test/java/org/apache/commons/io/IOExceptionListTestCase.java
new file mode 100644
index 0000000..7289129
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/IOExceptionListTestCase.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io;
+
+import java.io.EOFException;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IOExceptionListTestCase {
+
+    @Test
+    public void testCause() {
+        final EOFException cause = new EOFException();
+        final List<EOFException> list = Collections.singletonList(cause);
+        final IOExceptionList sqlExceptionList = new IOExceptionList(list);
+        Assert.assertEquals(cause, sqlExceptionList.getCause());
+        Assert.assertEquals(cause, sqlExceptionList.getCause(0));
+        Assert.assertEquals(list, sqlExceptionList.getCauseList());
+        Assert.assertEquals(list, sqlExceptionList.getCauseList(EOFException.class));
+        Assert.assertEquals(cause, sqlExceptionList.getCause(0, EOFException.class));
+        // No CCE:
+        final List<EOFException> causeList = sqlExceptionList.getCauseList();
+        Assert.assertEquals(list, causeList);
+    }
+
+    @Test
+    public void testNullCause() {
+        final IOExceptionList sqlExceptionList = new IOExceptionList(null);
+        Assert.assertNull(sqlExceptionList.getCause());
+        Assert.assertTrue(sqlExceptionList.getCauseList().isEmpty());
+    }
+
+    @Test
+    public void testPrintStackTrace() {
+        final EOFException cause = new EOFException();
+        final List<EOFException> list = Collections.singletonList(cause);
+        final IOExceptionList sqlExceptionList = new IOExceptionList(list);
+        sqlExceptionList.printStackTrace();
+    }
+}
diff --git a/src/test/java/org/apache/commons/io/IOIndexedExceptionTestCase.java b/src/test/java/org/apache/commons/io/IOIndexedExceptionTestCase.java
new file mode 100644
index 0000000..bed9157
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/IOIndexedExceptionTestCase.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io;
+
+import java.io.EOFException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests {@link IOIndexedException}.
+ *
+ * @since 2.7
+ */
+public class IOIndexedExceptionTestCase {
+
+    @Test
+    public void testEdge() {
+        final IOIndexedException exception = new IOIndexedException(-1, null);
+        Assert.assertEquals(-1, exception.getIndex());
+        Assert.assertEquals(null, exception.getCause());
+        Assert.assertNotNull(exception.getMessage());
+    }
+
+    @Test
+    public void testPlain() {
+        final EOFException e = new EOFException("end");
+        final IOIndexedException exception = new IOIndexedException(0, e);
+        Assert.assertEquals(0, exception.getIndex());
+        Assert.assertEquals(e, exception.getCause());
+        Assert.assertNotNull(exception.getMessage());
+    }
+}
diff --git a/src/test/java/org/apache/commons/io/output/ProxyCollectionWriterTest.java b/src/test/java/org/apache/commons/io/output/ProxyCollectionWriterTest.java
new file mode 100644
index 0000000..94fc4c4
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/output/ProxyCollectionWriterTest.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io.output;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.apache.commons.io.IOExceptionList;
+import org.apache.commons.io.IOIndexedException;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * JUnit Test Case for {@link ProxyCollectionWriter}.
+ */
+public class ProxyCollectionWriterTest {
+
+    @Test
+    public void testArrayIOExceptionOnAppendChar1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final char data = 'A';
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendChar2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final char data = 'A';
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequence1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final CharSequence data = "A";
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequence2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final CharSequence data = "A";
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequenceIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final CharSequence data = "A";
+        try {
+            tw.append(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequenceIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final CharSequence data = "A";
+        try {
+            tw.append(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnClose1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnClose2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnFlush1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        try {
+            tw.flush();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).flush();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnFlush2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        try {
+            tw.flush();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).flush();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArray1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArray2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArrayIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArrayIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final int data = 32;
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        try {
+            tw.write(32);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(32);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteString1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteString2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteStringIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteStringIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testCollectionCloseBranchIOException() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(Arrays.asList(goodW, badW, null));
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testConstructorsNull() throws IOException {
+        try (final ProxyCollectionWriter teeWriter = new ProxyCollectionWriter((Writer[]) null)) {
+            // Call any method, should not throw
+            teeWriter.append('a');
+            teeWriter.flush();
+        }
+        try (final ProxyCollectionWriter teeWriter = new ProxyCollectionWriter((Collection<Writer>) null)) {
+            // Call any method, should not throw
+            teeWriter.append('a');
+            teeWriter.flush();
+        }
+    }
+
+    @Test
+    public void testTee() throws IOException {
+        final StringBuilderWriter sbw1 = new StringBuilderWriter();
+        final StringBuilderWriter sbw2 = new StringBuilderWriter();
+        final StringBuilderWriter expected = new StringBuilderWriter();
+
+        try (final ProxyCollectionWriter tw = new ProxyCollectionWriter(sbw1, sbw2, null)) {
+            for (int i = 0; i < 20; i++) {
+                tw.write(i);
+                expected.write(i);
+            }
+            assertEquals("ProxyCollectionWriter.write(int)", expected.toString(), sbw1.toString());
+            assertEquals("ProxyCollectionWriter.write(int)", expected.toString(), sbw2.toString());
+
+            final char[] array = new char[10];
+            for (int i = 20; i < 30; i++) {
+                array[i - 20] = (char) i;
+            }
+            tw.write(array);
+            expected.write(array);
+            assertEquals("ProxyCollectionWriter.write(char[])", expected.toString(), sbw1.toString());
+            assertEquals("ProxyCollectionWriter.write(char[])", expected.toString(), sbw2.toString());
+
+            for (int i = 25; i < 35; i++) {
+                array[i - 25] = (char) i;
+            }
+            tw.write(array, 5, 5);
+            expected.write(array, 5, 5);
+            assertEquals("TeeOutputStream.write(byte[], int, int)", expected.toString(), sbw1.toString());
+            assertEquals("TeeOutputStream.write(byte[], int, int)", expected.toString(), sbw2.toString());
+
+            for (int i = 0; i < 20; i++) {
+                tw.append((char) i);
+                expected.append((char) i);
+            }
+            assertEquals("ProxyCollectionWriter.append(char)", expected.toString(), sbw1.toString());
+            assertEquals("ProxyCollectionWriter.append(char)", expected.toString(), sbw2.toString());
+
+            for (int i = 20; i < 30; i++) {
+                array[i - 20] = (char) i;
+            }
+            tw.append(new String(array));
+            expected.append(new String(array));
+            assertEquals("ProxyCollectionWriter.append(CharSequence)", expected.toString(), sbw1.toString());
+            assertEquals("ProxyCollectionWriter.write(CharSequence)", expected.toString(), sbw2.toString());
+
+            for (int i = 25; i < 35; i++) {
+                array[i - 25] = (char) i;
+            }
+            tw.append(new String(array), 5, 5);
+            expected.append(new String(array), 5, 5);
+            assertEquals("ProxyCollectionWriter.append(CharSequence, int, int)", expected.toString(), sbw1.toString());
+            assertEquals("ProxyCollectionWriter.append(CharSequence, int, int)", expected.toString(), sbw2.toString());
+
+            expected.flush();
+            expected.close();
+
+            tw.flush();
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/io/output/TeeWriterTest.java b/src/test/java/org/apache/commons/io/output/TeeWriterTest.java
new file mode 100644
index 0000000..90e028a
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/output/TeeWriterTest.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.io.output;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.apache.commons.io.IOExceptionList;
+import org.apache.commons.io.IOIndexedException;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * JUnit Test Case for {@link TeeWriter}.
+ */
+public class TeeWriterTest {
+
+    @Test
+    public void testArrayIOExceptionOnAppendChar1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(badW, goodW, null);
+        final char data = 'A';
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendChar2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final ProxyCollectionWriter tw = new ProxyCollectionWriter(goodW, badW, null);
+        final char data = 'A';
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequence1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequence2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.append(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequenceIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.append(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnAppendCharSequenceIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.append(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).append(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnClose1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnClose2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnFlush1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        try {
+            tw.flush();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).flush();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnFlush2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        try {
+            tw.flush();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).flush();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArray1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArray2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArrayIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteCharArrayIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final char[] data = new char[] { 'a' };
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final int data = 32;
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        try {
+            tw.write(32);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(32);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteString1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteString2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.write(data);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteStringIntInt1() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(badW, goodW, null);
+        final String data = "A";
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(0, e.getCause(0, IOIndexedException.class).getIndex());
+        }
+    }
+
+    @Test
+    public void testArrayIOExceptionOnWriteStringIntInt2() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(goodW, badW, null);
+        final String data = "A";
+        try {
+            tw.write(data, 0, 0);
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).write(data, 0, 0);
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testCollectionCloseBranchIOException() throws IOException {
+        final Writer badW = new BrokenWriter();
+        final StringWriter goodW = mock(StringWriter.class);
+        final TeeWriter tw = new TeeWriter(Arrays.asList(goodW, badW, null));
+        try {
+            tw.close();
+            fail("Expected " + IOException.class.getName());
+        } catch (final IOExceptionList e) {
+            verify(goodW).close();
+            Assert.assertEquals(1, e.getCauseList().size());
+            Assert.assertEquals(1, e.getCause(0, IOIndexedException.class).getIndex());
+
+        }
+    }
+
+    @Test
+    public void testConstructorsNull() throws IOException {
+        try (final TeeWriter teeWriter = new TeeWriter((Writer[]) null)) {
+            // Call any method, should not throw
+            teeWriter.append('a');
+            teeWriter.flush();
+        }
+        try (final TeeWriter teeWriter = new TeeWriter((Collection<Writer>) null)) {
+            // Call any method, should not throw
+            teeWriter.append('a');
+            teeWriter.flush();
+        }
+    }
+
+    @Test
+    public void testTee() throws IOException {
+        final StringBuilderWriter sbw1 = new StringBuilderWriter();
+        final StringBuilderWriter sbw2 = new StringBuilderWriter();
+        final StringBuilderWriter expected = new StringBuilderWriter();
+
+        try (final TeeWriter tw = new TeeWriter(sbw1, sbw2, null)) {
+            for (int i = 0; i < 20; i++) {
+                tw.write(i);
+                expected.write(i);
+            }
+            assertEquals("TeeWriter.write(int)", expected.toString(), sbw1.toString());
+            assertEquals("TeeWriter.write(int)", expected.toString(), sbw2.toString());
+
+            final char[] array = new char[10];
+            for (int i = 20; i < 30; i++) {
+                array[i - 20] = (char) i;
+            }
+            tw.write(array);
+            expected.write(array);
+            assertEquals("TeeWriter.write(char[])", expected.toString(), sbw1.toString());
+            assertEquals("TeeWriter.write(char[])", expected.toString(), sbw2.toString());
+
+            for (int i = 25; i < 35; i++) {
+                array[i - 25] = (char) i;
+            }
+            tw.write(array, 5, 5);
+            expected.write(array, 5, 5);
+            assertEquals("TeeOutputStream.write(byte[], int, int)", expected.toString(), sbw1.toString());
+            assertEquals("TeeOutputStream.write(byte[], int, int)", expected.toString(), sbw2.toString());
+
+            for (int i = 0; i < 20; i++) {
+                tw.append((char) i);
+                expected.append((char) i);
+            }
+            assertEquals("TeeWriter.append(char)", expected.toString(), sbw1.toString());
+            assertEquals("TeeWriter.append(char)", expected.toString(), sbw2.toString());
+
+            for (int i = 20; i < 30; i++) {
+                array[i - 20] = (char) i;
+            }
+            tw.append(new String(array));
+            expected.append(new String(array));
+            assertEquals("TeeWriter.append(CharSequence)", expected.toString(), sbw1.toString());
+            assertEquals("TeeWriter.write(CharSequence)", expected.toString(), sbw2.toString());
+
+            for (int i = 25; i < 35; i++) {
+                array[i - 25] = (char) i;
+            }
+            tw.append(new String(array), 5, 5);
+            expected.append(new String(array), 5, 5);
+            assertEquals("TeeWriter.append(CharSequence, int, int)", expected.toString(), sbw1.toString());
+            assertEquals("TeeWriter.append(CharSequence, int, int)", expected.toString(), sbw2.toString());
+
+            expected.flush();
+            expected.close();
+
+            tw.flush();
+        }
+    }
+
+}