You use case is a mixture of tools that are not quite available in the standard stream/reader implementations.
What makes is special is that you start with a reader process (lines of the encrypted files), then each line has to fall back to a bytes based process (Cipher operations), and you want the output to have character semantics (reader) to pass on to a CSV parser. This char/byte/char part is not trivial.
The process I would use is to create another reader, I call it the LineByLineProcessingReader
which allows to consume the input line by line, then process it, then make the output of each line available as a Reader.
The process really does not matter for this purpose. Your process implementation would be to Hex decode, then decypher, then convert back to string, but it might very well be just anything.
The tricky part is all in making the process conform to the Reader API.
When conforming to the Reader API, you have a choice, of extending FilterReader
or Reader
itself. The usual being the filter version.
I choose not to extend the filter version, because on the whole, my process is not to ever expose the original file's contents. As the filter's implementation always fallback to the orignal reader's, this maksing would imply re-implementing everything, which incurs a lot of work.
On the other hand, if I override Reader
directly, I only have one method to get right, because everything Reader
really has to express is the read(buf, o, c)
method, all other are implemented on top of it.
The strategy I chose to implement it, is akin to creating you own Iterator
implementation over some source of data. I prefetch a line of the input file, and make it available as a Reader
, in an internal variable.
That being done, I only need to make sure that this reader variable (e.g. current line) is always prefetched each time the previous has been fully read, just not before.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
public class LineByLineProcessingReader extends Reader {
/** What to use as a line break. Becuase bufferedReader does not report the line breaks, we insert them manually using this */
final String lineBreak;
/** The original input being read */
final BufferedReader input;
/** A reader for the current processed line. */
private StringReader currentReader;
private boolean closedOrFinished = false;
/**
* Creates a reader that will ingest an input line by line,
* then process it (default implementation is a no-op),
* then recreate a reader for it, until there is no more line.
*
* @param in a Reader object providing the underlying stream.
* @throws NullPointerException if <code>in</code> is <code>null</code>
*/
protected LineByLineProcessingReader(BufferedReader in, String lineBreak) {
this.input = in;
this.lineBreak = lineBreak;
}
public int read(char[] cbuf, int off, int len) throws IOException {
ensureNextLine();
// Check end of input
if (currentReader == null) {
return -1;
}
int read = currentReader.read(cbuf, off, len);
// Edge case : if current reader was at its end
if (read < 0) {
currentReader = null;
// Recurse to go fetch next line.
return read(cbuf, off, len);
}
// General case, we have our result.
// We may have read less than was asked (in length), but it's contractually OK.
return read;
}
/**
* Advances the underlying input to the next line, and makes it available
* for reading inside the {@link #currentReader}
*/
private void ensureNextLine() throws IOException {
// Do not try to read if closed or already finished
if (closedOrFinished) {
return;
}
// Check if there is still data to be read
if (currentReader != null) {
return;
}
String nextLine = input.readLine();
if (nextLine == null) {
// Nothing was left to read, we are bailing out.
currentReader = null;
closedOrFinished = true;
return;
}
// We have a new line, process it and publish it as a reader
String processedLine = processRawLine(nextLine);
currentReader = new StringReader(processedLine+lineBreak);
}
/**
* Performs a process of the raw line read from the underlying source.
* @param rawLine the raw line read
* @return a processed line
*/
protected String processRawLine(String rawLine) {
return rawLine;
}
@Override
public void close() throws IOException {
input.close();
closedOrFinished = true;
}
}
What would be left to do is plug your decryption process inside the processLine
method.
A very quick test for the class (you might want to check it further).
import junit.framework.TestCase;
import org.junit.Assert;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LineByLineProcessingReaderTest extends TestCase {
public void testRead() throws IOException {
String input = "a\nb";
// Reading a char one by one
try (Reader r = new LineByLineProcessingReader(new BufferedReader(new StringReader(input)), "\n")) {
String oneByOne = readExcatlyCharByChar(r, 3);
Assert.assertEquals(input, oneByOne);
}
// Reading lines
List<String> lines = readAllLines(
new LineByLineProcessingReader(
new BufferedReader(new StringReader(input)),
"\n"
)
);
Assert.assertEquals(Arrays.asList("a", "b"), lines);
String[] moreComplexInput = new String[] {"Two households, both alike in dignity",
"In fair Verona, where we lay our scene",
"From ancient grudge break to new mutiny",
"Where civil blood makes civil hands unclean." +
"From forth the fatal loins of these two foes" +
"A pair of star-cross'd lovers take their life;" +
"Whose misadventured piteous overthrows" +
"Do with their death bury their parents' strife." +
"The fearful passage of their death-mark'd love",
"And the continuance of their parents' rage",
"Which, but their children's end, nought could remove",
"Is now the two hours' traffic of our stage;" +
"The which if you with patient ears attend",
"What here shall miss, our toil shall strive to mend."};
lines = readAllLines(new LineByLineProcessingReader(
new BufferedReader(new StringReader(String.join("\n", moreComplexInput))), "\n") {
@Override
protected String processRawLine(String rawLine) {
return rawLine.toUpperCase();
}
});
Assert.assertEquals(Arrays.stream(moreComplexInput).map(String::toUpperCase).collect(Collectors.toList()), lines);
}
private String readExcatlyCharByChar(Reader reader,int numberOfReads) throws IOException {
int nbRead = 0;
try (StringWriter output = new StringWriter()) {
while (nbRead < numberOfReads) {
int read = reader.read();
if (read < 0) {
throw new IOException("Expected " + numberOfReads + " but were only " + nbRead + " available");
}
output.write(read);
nbRead++;
}
return output.toString();
}
}
private List<String> readAllLines(Reader reader) throws IOException {
try (BufferedReader b = new BufferedReader(reader)) {
return b.lines().collect(Collectors.toList());
}
}
}