/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io.compound;
import java.io.IOException;
import java.io.Reader;
/**
* <code>CompoundReader</code> concatenates several readers into one. It can operate in two modes:
* <dl>
* <dt>Merged</dt>
* <dd>the compound reader acts as a single, global reader, merging the contents of underlying readers. The
* compound reader can be read just like a regular <code>Reader</code> -- readers are advanced automatically as EOF
* of individual readers are reached.</dd>
*
* <dt>Unmerged</dt>
* <dd>the compound reader has be advanced manually. EOF are signaled individually for each underlying reader.
* After EOF is reached, the current reader has to be advanced to the next one using {@link #advanceReader()}.</dd>
* <dl>
*
* <p>
* This class is abstract, with a single method to implement: {@link #getNextReader()}.
* See {@link IteratorCompoundReader} for an <code>Iterator</code>-backed implementation.
* </p>
*
* @see IteratorCompoundReader
* @see CompoundInputStream
* @author Maxence Bernard
*/
public abstract class CompoundReader extends Reader {
/** True if this CompoundReader operates in 'merged' mode */
private boolean merged;
/** The Reader that's currently being processed */
private Reader currentReader;
/** Used by {@link #read()} */
private char oneCharBuf[];
/** <code>true</code> if the global EOF has been reached */
private boolean globalEOFReached;
/**
* Creates a new <code>CompoundReader</code> operating in the specified mode.
*
* @param merged <code>true</code> if the readers should be merged, acting as a single reader, or considered
* as separate readers that have to be {@link #advanceReader() advanced manually}.
*/
public CompoundReader(boolean merged) {
this.merged = merged;
}
/**
* Returns:
* <ul>
* <li><code>true</code> if this reader acts as a single, global input reader, merging the contents of underlying
* readers. In this mode, the compound reader can be read just like a regular Reader -- readers are advanced
* automatically as EOF of individual readers are reached.</li>
* <li><code>false</code> if this reader has be advanced manually. In this mode, EOF are signaled individually
* for each underlying reader. After EOF has been reached, the current reader has to be advanced to the next one
* using {@link #advanceReader()}.</li>
* </ul>
*
* @return <code>true</code> if this reader acts as a global reader, <code>false</code> if this reader has
* to be advanced manually.
*/
public boolean isMerged() {
return merged;
}
/**
* Returns the <code>Reader</code> this compound reader is currently reading, <code>null</code> if this reader
* hasn't read anything yet, or if it has no underlying reader to read.
*
* @return <code>Reader</code> this compound reader is currently reading
*/
public Reader getCurrentReader() {
return currentReader;
}
/**
* Closes the current reader, if any. This method has no effect if there is no current reader.
*
* @throws IOException if an error occurred while closing the current reader.
*/
public void closeCurrentReader() throws IOException {
if(currentReader!=null)
currentReader.close();
}
/**
* Advances the current reader to the next one, causing subsequent calls to <code>Reader</code>
* methods to operate on the new reader. Returns <code>true</code> if there was a next reader, <code>false</code>
* otherwise.
* <p>
* Note: the current reader (if any) will be closed by this method.
* </p>
*
* @return <code>true</code> if there was a next reader, <code>false</code> otherwise
* @throws IOException if an error occurred while trying to advancing the current reader. This
* <code>CompoundReader</code> can't be used after that and must be closed.
*/
public boolean advanceReader() throws IOException {
// Return immediately (don't close the reader) if this method is global EOF has already been reached
if(globalEOFReached)
return false;
// Close the current reader
if(currentReader !=null) {
try {
closeCurrentReader();
}
catch(IOException e) {
// Fail silently
}
}
// Try to advance the current Reader to the next
try {
currentReader = getNextReader();
}
catch(IOException e) {
// Can't recover from this, this is the end of this stream
globalEOFReached = true;
throw e;
}
if(currentReader==null) {
// Global EOF reached
globalEOFReached = true;
return false;
}
return true;
}
/**
* Checks the current reader and returns <code>true</code> if the current reader is in a state where it can be
* accessed, <code>false</code> if global EOF has been reached.
*
* @return <code>true</code> if the current reader is in a state where it can be accessed, <code>false</code> if
* global EOF has been reached.
* @throws IOException if an error occurred while trying to advancing the current reader.
*/
private boolean checkReader() throws IOException {
if(globalEOFReached)
return true;
if(currentReader ==null)
if(!advanceReader())
return true;
return false;
}
//////////////////////
// Abstract methods //
//////////////////////
/**
* Returns the next <code>Reader</code>, <code>null</code> if there is none.
* <p>
* Before calling this method, {@link #advanceReader()} closes the current reader (if any). In other words,
* implementations do not have to worry about closing previously-returned readers.
* </p>
*
* @return the next <code>Reader</code>, <code>null</code> if there is none.
* @throws IOException if an error occurred while retrieving the next reader
*/
public abstract Reader getNextReader() throws IOException;
///////////////////////////
// Reader implementation //
///////////////////////////
/**
* Delegates to {@link #read(char[], int, int)} with a 1-char buffer.
*/
@Override
public int read() throws IOException {
if(oneCharBuf ==null)
oneCharBuf = new char[1];
int ret = read(oneCharBuf, 0, 1);
return ret<=0?ret:oneCharBuf[0];
}
/**
* Delegates to {@link #read(char[], int, int)} with a <code>0</code> offset and the whole buffer's length.
*/
@Override
public int read(char[] c) throws IOException {
return read(c, 0, c.length);
}
/**
* Reads up to <code>len-off</code> characters and stores them in the specified buffer, starting at <code>off</code>.
* Returns the number of characters that were actually read, or <code>-1</code> to signal:
* <ul>
* <li>if {@link #isMerged()} is enabled, the end of the compound reader as a whole</li>
* <li>if {@link #isMerged ()} is disabled, the end of the current reader, which may or may not coincide
* with the end of the reader as a whole.</li>
* </ul>
*/
@Override
public int read(char[] c, int off, int len) throws IOException {
if(checkReader())
return -1;
int ret = currentReader.read(c, off, len);
if(ret==-1) {
// read the next reader
if(merged) {
if(!advanceReader())
return -1; // Global EOF reached
// Recurse
return read(c, off, len);
}
return -1;
}
return ret;
}
/**
* Skips up to <code>n</code> characters and returns the number of characters that were actually skipped, or
* <code>-1</code> to signal:
* <ul>
* <li>if {@link #isMerged()} is enabled, the end of the compound reader as a whole</li>
* <li>if {@link #isMerged ()} is disabled, the end of the current reader, which may or may not coincide
* with the end of the reader as a whole.</li>
* </ul>
*/
@Override
public long skip(long n) throws IOException {
if(checkReader())
return -1;
long ret = currentReader.skip(n);
if(ret==-1) {
// read the next reader
if(merged) {
if(!advanceReader())
return -1; // Global EOF reached
return currentReader.skip(n);
}
return -1;
}
return ret;
}
/**
* Closes the current <code>Reader</code> and this <code>CompoundReader</code> a whole.
* The current reader can no longer be advanced after this method has been called.
*
* @throws IOException if the current reader could not be closed.
*/
@Override
public void close() throws IOException {
try {
if(currentReader!=null)
closeCurrentReader();
}
finally {
globalEOFReached = true;
}
}
/**
* Delegates to the current <code>Reader</code>.
*/
@Override
public boolean ready() throws IOException {
return !checkReader() && currentReader.ready();
}
/**
* Delegates to the current <code>Reader</code>.
*/
@Override
public void mark(int readlimit) throws IOException {
if(!checkReader())
currentReader.mark(readlimit);
}
/**
* Delegates to the current <code>Reader</code>.
*/
@Override
public void reset() throws IOException {
if(!checkReader())
currentReader.reset();
}
/**
* Delegates to the current <code>Reader</code>.
*/
@Override
public boolean markSupported() {
try {
return !checkReader() && currentReader.markSupported();
}
catch(IOException e) {
return false;
}
}
}