package tap.data;
/*
* This file is part of TAPLibrary.
*
* ADQLLibrary 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.
*
* ADQLLibrary 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 ADQLLibrary. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2014 - Astronomisches Rechen Institut (ARI)
*/
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.NoSuchElementException;
import tap.ServiceConnection.LimitUnit;
import tap.metadata.TAPColumn;
import tap.upload.LimitedSizeInputStream;
import adql.db.DBType;
import com.oreilly.servlet.multipart.ExceededSizeException;
/**
* <p>Wrap a {@link TableIterator} in order to limit its reading to a fixed number of rows.</p>
*
* <p>
* This wrapper can be "mixed" with a {@link LimitedSizeInputStream}, by wrapping the original input stream by a {@link LimitedSizeInputStream}
* and then by wrapping the {@link TableIterator} based on this wrapped input stream by {@link LimitedTableIterator}.
* Thus, this wrapper will be able to detect embedded {@link ExceededSizeException} thrown by a {@link LimitedSizeInputStream} through another {@link TableIterator}.
* If a such exception is detected, it will declare this wrapper as overflowed as it would be if a rows limit is reached.
* </p>
*
* <p><b>Warning:</b>
* To work together with a {@link LimitedSizeInputStream}, this wrapper relies on the hypothesis that any {@link IOException} (including {@link ExceededSizeException})
* will be embedded in a {@link DataReadException} as cause of this exception (using {@link DataReadException#DataReadException(Throwable)}
* or {@link DataReadException#DataReadException(String, Throwable)}). If it is not the case, no overflow detection could be done and the exception will just be forwarded.
* </p>
*
* <p>
* If a limit - either of rows or of bytes - is reached, a flag "overflow" is set to true. This flag can be got with {@link #isOverflow()}.
* Thus, when a {@link DataReadException} is caught, it will be easy to detect whether the error occurred because of an overflow
* or of another problem.
* </p>
*
* @author Grégory Mantelet (ARI)
* @version 2.0 (01/2015)
* @since 2.0
*/
public class LimitedTableIterator implements TableIterator {
/** The wrapped {@link TableIterator}. */
private final TableIterator innerIt;
/** Limit on the number of rows to read. <i>note: a negative value means "no limit".</i> */
private final int maxNbRows;
/** The number of rows already read. */
private int countRow = 0;
/** Indicate whether a limit (rows or bytes) has been reached or not. */
private boolean overflow = false;
/**
* Wrap the given {@link TableIterator} so that limiting the number of rows to read.
*
* @param it The iterator to wrap. <i>MUST NOT be NULL</i>
* @param nbMaxRows Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. <i>A negative value means "no limit".</i>
*/
public LimitedTableIterator(final TableIterator it, final int nbMaxRows) throws DataReadException{
if (it == null)
throw new NullPointerException("Missing TableIterator to wrap!");
innerIt = it;
this.maxNbRows = nbMaxRows;
}
/**
* <p>Build the specified {@link TableIterator} instance and wrap it so that limiting the number of rows OR bytes to read.</p>
*
* <p>
* If the limit is on the <b>number of bytes</b>, the given input stream will be first wrapped inside a {@link LimitedSizeInputStream}.
* Then, it will be given as only parameter of the constructor of the specified {@link TableIterator} instance.
* </p>
*
* <p>If the limit is on the <b>number of rows</b>, this {@link LimitedTableIterator} will count and limit itself the number of rows.</p>
*
* <p><i><b>IMPORTANT:</b> The specified class must:</i></p>
* <i><ul>
* <li>extend {@link TableIterator},</li>
* <li>be a concrete class,</li>
* <li>have at least one constructor with only one parameter of type {@link InputStream}.</li>
* </ul></i>
*
* <p><i>Note:
* If the given limit type is NULL (or different from ROWS and BYTES), or the limit value is <=0, no limit will be set.
* All rows and bytes will be read until the end of input is reached.
* </i></p>
*
* @param classIt Class of the {@link TableIterator} implementation to create and whose the output must be limited.
* @param input Input stream toward the table to read.
* @param type Type of the limit: ROWS or BYTES. <i>MAY be NULL</i>
* @param limit Limit in rows or bytes, depending of the "type" parameter. <i>MAY BE <=0</i>
*
* @throws DataReadException If no instance of the given class can be created,
* or if the {@link TableIterator} instance can not be initialized,
* or if the limit (in rows or bytes) has been reached.
*/
public < T extends TableIterator > LimitedTableIterator(final Class<T> classIt, final InputStream input, final LimitUnit type, final int limit) throws DataReadException{
try{
Constructor<T> construct = classIt.getConstructor(InputStream.class);
if (LimitUnit.bytes.isCompatibleWith(type) && limit > 0){
maxNbRows = -1;
innerIt = construct.newInstance(new LimitedSizeInputStream(input, limit * type.bytesFactor()));
}else{
innerIt = construct.newInstance(input);
maxNbRows = (type == null || type != LimitUnit.rows) ? -1 : limit;
}
}catch(InvocationTargetException ite){
Throwable t = ite.getCause();
if (t != null && t instanceof DataReadException){
ExceededSizeException exceedEx = getExceededSizeException(t);
// if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception:
if (exceedEx != null)
throw new DataReadException(exceedEx.getMessage(), exceedEx);
else
throw (DataReadException)t;
}else
throw new DataReadException("Can not create a LimitedTableIterator!", ite);
}catch(Exception ex){
throw new DataReadException("Can not create a LimitedTableIterator!", ex);
}
}
/**
* Get the iterator wrapped by this {@link TableIterator} instance.
*
* @return The wrapped iterator.
*/
public final TableIterator getWrappedIterator(){
return innerIt;
}
/**
* <p>Tell whether a limit (in rows or bytes) has been reached.</p>
*
* <p><i>Note:
* If <i>true</i> is returned (that's to say, if a limit has been reached) no more rows or column values
* can be read ; an {@link IllegalStateException} would then be thrown.
* </i></p>
*
* @return <i>true</i> if a limit has been reached, <i>false</i> otherwise.
*/
public final boolean isOverflow(){
return overflow;
}
@Override
public void close() throws DataReadException{
innerIt.close();
}
@Override
public TAPColumn[] getMetadata() throws DataReadException{
return innerIt.getMetadata();
}
@Override
public boolean nextRow() throws DataReadException{
// Test the overflow flag and proceed only if not overflowed:
if (overflow)
throw new DataReadException("Data read overflow: the limit has already been reached! No more data can be read.");
// Read the next row:
boolean nextRow;
try{
nextRow = innerIt.nextRow();
countRow++;
}catch(DataReadException ex){
ExceededSizeException exceedEx = getExceededSizeException(ex);
// if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception:
if (exceedEx != null){
overflow = true;
throw new DataReadException(exceedEx.getMessage());
}else
throw ex;
}
// If, counting this one, the number of rows exceeds the limit, set this iterator as overflowed and throw an exception:
if (nextRow && maxNbRows >= 0 && countRow > maxNbRows){
overflow = true;
throw new DataReadException("Data read overflow: the limit of " + maxNbRows + " rows has been reached!");
}
// Send back the value returned by the inner iterator:
return nextRow;
}
@Override
public boolean hasNextCol() throws IllegalStateException, DataReadException{
testOverflow();
return innerIt.hasNextCol();
}
@Override
public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
testOverflow();
return innerIt.nextCol();
}
@Override
public DBType getColType() throws IllegalStateException, DataReadException{
testOverflow();
return innerIt.getColType();
}
/**
* Test the overflow flag and throw an {@link IllegalStateException} if <i>true</i>.
*
* @throws IllegalStateException If this iterator is overflowed (because of either a bytes limit or a rows limit).
*/
private void testOverflow() throws IllegalStateException{
if (overflow)
throw new IllegalStateException("Data read overflow: the limit has already been reached! No more data can be read.");
}
/**
* Get the first {@link ExceededSizeException} found in the given {@link Throwable} trace.
*
* @param ex A {@link Throwable}
*
* @return The first {@link ExceededSizeException} encountered, or NULL if none has been found.
*/
private ExceededSizeException getExceededSizeException(Throwable ex){
if (ex == null)
return null;
while(!(ex instanceof ExceededSizeException) && ex.getCause() != null)
ex = ex.getCause();
return (ex instanceof ExceededSizeException) ? (ExceededSizeException)ex : null;
}
}