TeeReader.java

/*
 * 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.input;

import static org.apache.commons.io.IOUtils.EOF;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.CharBuffer;

/**
 * Reader proxy that transparently writes a copy of all characters read from the proxied reader to a given Reader. Using
 * {@link #skip(long)} or {@link #mark(int)}/{@link #reset()} on the reader will result on some characters from the
 * reader being skipped or duplicated in the writer.
 * <p>
 * The proxied reader is closed when the {@link #close()} method is called on this proxy. You may configure whether the
 * reader closes the writer.
 * </p>
 *
 * @since 2.7
 */
public class TeeReader extends ProxyReader {

    /**
     * The writer that will receive a copy of all characters read from the proxied reader.
     */
    private final Writer branch;

    /**
     * Flag for closing the associated writer when this reader is closed.
     */
    private final boolean closeBranch;

    /**
     * Constructs a TeeReader that proxies the given {@link Reader} and copies all read characters to the given
     * {@link Writer}. The given writer will not be closed when this reader gets closed.
     *
     * @param input  reader to be proxied
     * @param branch writer that will receive a copy of all characters read
     */
    public TeeReader(final Reader input, final Writer branch) {
        this(input, branch, false);
    }

    /**
     * Constructs a TeeReader that proxies the given {@link Reader} and copies all read characters to the given
     * {@link Writer}. The given writer will be closed when this reader gets closed if the closeBranch parameter is
     * {@code true}.
     *
     * @param input       reader to be proxied
     * @param branch      writer that will receive a copy of all characters read
     * @param closeBranch flag for closing also the writer when this reader is closed
     */
    public TeeReader(final Reader input, final Writer branch, final boolean closeBranch) {
        super(input);
        this.branch = branch;
        this.closeBranch = closeBranch;
    }

    /**
     * Closes the proxied reader and, if so configured, the associated writer. An exception thrown from the reader will
     * not prevent closing of the writer.
     *
     * @throws IOException if either the reader or writer could not be closed
     */
    @Override
    public void close() throws IOException {
        try {
            super.close();
        } finally {
            if (closeBranch) {
                branch.close();
            }
        }
    }

    /**
     * Reads a single character from the proxied reader and writes it to the associated writer.
     *
     * @return next character from the reader, or -1 if the reader has ended
     * @throws IOException if the reader could not be read (or written)
     */
    @Override
    public int read() throws IOException {
        final int ch = super.read();
        if (ch != EOF) {
            branch.write(ch);
        }
        return ch;
    }

    /**
     * Reads characters from the proxied reader and writes the read characters to the associated writer.
     *
     * @param chr character buffer
     * @return number of characters read, or -1 if the reader has ended
     * @throws IOException if the reader could not be read (or written)
     */
    @Override
    public int read(final char[] chr) throws IOException {
        final int n = super.read(chr);
        if (n != EOF) {
            branch.write(chr, 0, n);
        }
        return n;
    }

    /**
     * Reads characters from the proxied reader and writes the read characters to the associated writer.
     *
     * @param chr character buffer
     * @param st  start offset within the buffer
     * @param end maximum number of characters to read
     * @return number of characters read, or -1 if the reader has ended
     * @throws IOException if the reader could not be read (or written)
     */
    @Override
    public int read(final char[] chr, final int st, final int end) throws IOException {
        final int n = super.read(chr, st, end);
        if (n != EOF) {
            branch.write(chr, st, n);
        }
        return n;
    }

    /**
     * Reads characters from the proxied reader and writes the read characters to the associated writer.
     *
     * @param target character buffer
     * @return number of characters read, or -1 if the reader has ended
     * @throws IOException if the reader could not be read (or written)
     */
    @Override
    public int read(final CharBuffer target) throws IOException {
        final int originalPosition = target.position();
        final int n = super.read(target);
        if (n != EOF) {
            // Appending can only be done after resetting the CharBuffer to the
            // right position and limit.
            final int newPosition = target.position();
            final int newLimit = target.limit();
            try {
                target.position(originalPosition).limit(newPosition);
                branch.append(target);
            } finally {
                // Reset the CharBuffer as if the appending never happened.
                target.position(newPosition).limit(newLimit);
            }
        }
        return n;
    }

}