/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotoolkit.image.io;
import java.io.*; // Many imports, including some for javadoc only.
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Locale;
import java.util.Collections;
import java.text.ParseException;
import java.awt.image.BufferedImage;
import javax.imageio.ImageReadParam;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.io.LineFormat;
/**
* A dummy implementation of {@link TextImageReader} used only by default implementation
* of {@link TextImageReader.Spi#canDecodeInput}. This class is more lightweight than
* loading the real image reader implementation.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @since 3.08 (derived from 2.4)
* @module
*/
final class TestReader extends TextImageReader {
/**
* The input stream to {@linkplain InputStream#reset reset}, or {@code null} if none. This
* field may be assigned by {@link #getReader()}. If it is non-null, then the {@code reset()}
* method of this stream shall be invoked instead than {@link Reader#reset()}.
* <p>
* This stream will never be closed by this {@code TestReader} class, i.e. it should never
* be the same instance than {@link #closeOnReset}.
*/
private InputStream marked;
/**
* The keys found in the header (without value), or {@code null} if none.
*/
private Set<String> keywords;
/**
* The object to use for parsing the lines, or {@code null} if not yet created.
*/
private LineFormat parser;
/**
* The rows which have been parsed, or {@code null} if not yet created.
*/
private double[][] rows;
/**
* The number of valid entries in {@link #rowCount}.
*/
private int rowCount;
/**
* Creates a new reader for the specified provider.
* The provider is mandatory and can not be null.
*/
public TestReader(final TextImageReader.Spi provider) {
super(provider);
}
/**
* Returns a null width.
*/
@Override
public int getWidth(int imageIndex) {
return 0;
}
/**
* Returns a null height.
*/
@Override
public int getHeight(int imageIndex) {
return 0;
}
/**
* Throws an {@link UnsupportedOperationException}.
*/
@Override
public BufferedImage read(final int imageIndex, final ImageReadParam param) {
throw new UnsupportedOperationException();
}
/**
* Returns the {@linkplain #input input} as a {@linkplain Reader reader}, which doesn't need to
* be {@linkplain BufferedReader buffered}. If the reader is an instance supplied explicitly by
* the user, then it will be {@linkplain Reader#mark marked} with the specified read ahead limit.
*
* @return {@link #getInput} as a {@link Reader}, or {@code null} if this method
* can't provide a reader suitable for {@code canDecode}.
* @throws IllegalStateException if the {@linkplain #input input} is not set.
* @throws IOException If the input stream can't be created for an other reason.
*/
private Reader getReader(final int readAheadLimit) throws IllegalStateException, IOException {
final Object input = getInput();
if (input instanceof Reader) {
final Reader reader = (Reader) input;
if (!reader.markSupported()) {
return null;
}
reader.mark(readAheadLimit);
return reader;
// Do not set 'closeOnReset' since we don't own the reader.
}
final InputStream stream = getInputStream();
if (closeOnReset == null) {
// If we are not allowed to close and reopen a new stream on ImageReader.read, then
// we must be able to mark the stream otherwise we will not support canDecode(...).
if (!stream.markSupported()) {
return null;
}
stream.mark(readAheadLimit);
marked = stream;
}
final Reader reader = getInputStreamReader(stream);
if (closeOnReset == stream) {
closeOnReset = reader;
}
return reader;
}
/**
* Resets the stream to the marked position.
*/
private void reset(final Reader reader) throws IOException {
final InputStream m = marked;
if (m != null) {
marked = null;
m.reset();
// Do not close the Reader, since we don't
// want to close the underlying InputStream.
} else if (reader != null && reader != closeOnReset) {
reader.reset();
} else {
super.close();
}
}
/**
* Checks if the {@linkplain #getReader reader} seems to contains a readable ASCII file.
* This method tries to read the first few lines. The caller is responsable for invoking
* {@link #close} after this method.
*
* {@section How to change the default behavior}
* This method invokes {@link #parseLine(String)} for each line found. When we have
* reached the {@code readAheadLimit}, this method invokes {@link #isValidContent()}.
* The default implementations are:
* <p>
* <ul>
* <li>
* {@code parseLine} skips the comment lines and stores the values in the
* {@link #rows} field.</li>
* <li>
* {@code isValidContent} delegates to the method below with the data
* that has been collected in the previous step:
* <ul>
* <li>{@link TextImageReader.Spi#isValidHeader(Set)}</li>
* <li>{@link TextImageReader.Spi#isValidContent(double[][])}</li>
* </ul>
* </li>
* </ul>
* <p>
* If a different behavior is wanted, those two methods should be made accessible and overridden.
*
* @param readAheadLimit Maximum number of characters to read. If this amount is reached
* but this method still unable to make a choice, then it returns {@code null}.
* @return {@code true} if the source <em>seems</em> readable, {@code false} otherwise.
* @throws IOException If an error occurred during reading.
*/
final boolean canDecode(int readAheadLimit) throws IOException {
final Reader input = getReader(readAheadLimit);
if (input == null) {
return false;
}
/*
* Cheap test first: read only a few bytes an check if there is characters which seem
* to be binary. If we pass this cheap test, then fill the remaining of the buffer and
* check again.
*/
final char[] buffer = new char[readAheadLimit];
readAheadLimit = input.read(buffer, 0, Math.min(readAheadLimit, 256));
if (readAheadLimit < 0 || containsBinary(buffer, 0, readAheadLimit)) {
reset(input);
return false;
}
if (true) { // Set to 'false' if we want to use only the above 256 characters.
final int more = input.read(buffer, readAheadLimit, buffer.length - readAheadLimit);
if (more >= 0) {
if (containsBinary(buffer, readAheadLimit, readAheadLimit + more)) {
reset(input);
return false;
}
readAheadLimit += more;
}
}
/*
* At this point we have determined that the stream is (apparently) not binary.
* Now parse the content of the above buffer.
*/
int lower = 0;
scan: while (lower < readAheadLimit) {
// Skip line feeds at the beginning of the line. They may be
// trailing characters from the previous iteration of the loop.
char c = buffer[lower];
if (c == '\r' || c == '\n') {
lower++;
continue;
}
// Search the end of line. If we reach the end of the buffer,
// do not attempt to parse that last line since it is incomplete.
int upper = lower;
while ((c = buffer[upper]) != '\r' && c != '\n') {
if (++upper >= readAheadLimit) {
break scan;
}
}
// Try to parse a line.
final String line = new String(buffer, lower, upper-lower);
if (!isComment(line) && !parseLine(line)) {
reset(input);
return false;
}
lower = upper;
}
reset(input);
return isValidContent();
}
/**
* Returns {@code true} if the given range in the given buffer seems to contains binary data.
* This is a cheap test before the more expensive parsing of data.
*/
private static boolean containsBinary(final char[] buffer, int lower, final int upper) {
while (lower < upper) {
final char c = buffer[lower++];
if (c < 32 && !Character.isWhitespace(c)) {
return true;
}
}
return false;
}
/**
* Invoked by {@link #canDecode(int)} for each line found before the {@code readAheadLimit}.
* The default implementation stores the first word in the {@linkplain #keywords} set if it
* seem to be an identifier, or parses the line as a row of numbers and store the result in
* the {@link #rows} field otherwise.
*
* @param line The line to parse.
* @return {@code true} if the line is valid, or {@code false} otherwise.
* @throws IOException If an error occurred while processing the line.
*
* @since 3.07
*/
private boolean parseLine(final String line) throws IOException {
/*
* First, check if we have a header keyword. We check the header only
* if the parsing of rows did not started yet. Headers after the rows
* are not allowed.
*/
if (rows == null) {
String keyword = line.trim();
final int length = keyword.length();
if (length == 0) {
// Ignore blank lines. Note that some formats could consider blank lines as
// the beginning of next band or next image. This TestReader just ignore them.
return true;
}
if (Character.isJavaIdentifierStart(keyword.charAt(0))) {
int stop = 0;
while (++stop < length && Character.isJavaIdentifierPart(keyword.charAt(stop)));
keyword = keyword.substring(0, stop);
if (keywords == null) {
keywords = new HashSet<>();
}
final Locale locale = getDataLocale();
keyword = (locale != null) ? keyword.toUpperCase(locale) : keyword.toUpperCase();
keywords.add(keyword);
return true;
}
}
/*
* If we reach this point, the line is not considered a header.
* Try to parse it as a row of pixel values.
*/
if (parser == null) {
parser = getLineFormat(0);
rows = new double[16][];
}
try {
if (parser.setLine(line) != 0) {
if (rowCount == rows.length) {
rows = Arrays.copyOf(rows, rows.length * 2);
}
rows[rowCount] = parser.getValues(rows[rowCount]);
rowCount++;
}
} catch (ParseException exception) {
return false;
}
return true;
}
/**
* Invoked by {@link #canDecode(int)} after every lines have been parsed. The default
* implementation gives {@link #keywords} to {@link TextImageReader.Spi#isValidHader(Set)}
* and the {@link #rows} to {@link TextImageReader.Spi#isValidContent(double[][])}.
*
* @return {@code true} if the content is valid, or {@code false} otherwise.
* @throws IOException If an error occurred.
*
* @since 3.07
*/
private boolean isValidContent() throws IOException {
final TextImageReader.Spi spi = (TextImageReader.Spi) originatingProvider;
if (keywords == null) {
keywords = Collections.emptySet();
}
if (spi.isValidHeader(keywords)) {
if (rows == null) {
rows = new double[0][];
} else {
rows = ArraysExt.resize(rows, rowCount);
}
return spi.isValidContent(rows);
}
return false;
}
/**
* Closes the reader created by this class.
*/
@Override
protected void close() throws IOException {
reset(null);
marked = null;
keywords = null;
parser = null;
rows = null;
rowCount = 0;
super.close();
}
}