package fj.data; import static fj.Bottom.errorF; import static fj.Function.constant; import static fj.Function.partialApply2; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.Arrays; import fj.F; import fj.Function; import fj.P; import fj.P1; import fj.P2; import fj.Unit; import fj.data.Iteratee.Input; import fj.data.Iteratee.IterV; /** * IO monad for processing files, with main methods {@link #enumFileLines(File, Option, IterV)}, * {@link #enumFileChars(File, Option, IterV)} and {@link #enumFileCharChunks(File, Option, IterV)} * (the latter one is the fastest as char chunks read from the file are directly passed to the iteratee * without indirection in between). * * @author Martin Grotzke * * @param the type of the result produced by the wrapped iteratee */ public abstract class IO { private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; public static final F> closeReader = new F>() { @Override public IO f(final Reader r) { return closeReader(r); } }; public static IO closeReader(final Reader r) { return new IO() { @Override public Unit run() throws IOException { r.close(); return Unit.unit(); } }; } /** * An IO monad that reads lines from the given file (using a {@link BufferedReader}) and passes * lines to the provided iteratee. May not be suitable for files with very long * lines, consider to use {@link #enumFileCharChunks(File, IterV)} or {@link #enumFileChars(File, IterV)} * as an alternative. * * @param f the file to read, must not be null * @param encoding the encoding to use, {@link Option#none()} means platform default * @param i the iteratee that is fed with lines read from the file */ public static IO> enumFileLines(final File f, final Option encoding, final IterV i) { return bracket(bufferedReader(f, encoding) , Function.>vary(closeReader) , partialApply2(IO.lineReader(), i)); } /** * An IO monad that reads char chunks from the given file and passes them to the given iteratee. * * @param f the file to read, must not be null * @param encoding the encoding to use, {@link Option#none()} means platform default * @param i the iteratee that is fed with char chunks read from the file */ public static IO> enumFileCharChunks(final File f, final Option encoding, final IterV i) { return bracket(fileReader(f, encoding) , Function.>vary(closeReader) , partialApply2(IO.charChunkReader(), i)); } /** * An IO monad that reads char chunks from the given file and passes single chars to the given iteratee. * * @param f the file to read, must not be null * @param encoding the encoding to use, {@link Option#none()} means platform default * @param i the iteratee that is fed with chars read from the file */ public static IO> enumFileChars(final File f, final Option encoding, final IterV i) { return bracket(fileReader(f, encoding) , Function.>vary(closeReader) , partialApply2(IO.charChunkReader2(), i)); } public static IO bufferedReader(final File f, final Option encoding) { return fileReader(f, encoding).map(new F() { @Override public BufferedReader f(final Reader a) { return new BufferedReader(a); }}); } public static IO fileReader(final File f, final Option encoding) { return new IO() { @Override public Reader run() throws IOException { final FileInputStream fis = new FileInputStream(f); return encoding.isNone() ? new InputStreamReader(fis) : new InputStreamReader(fis, encoding.some()); } }; } public static final IO bracket(final IO init, final F> fin, final F> body) { return new IO() { @Override public C run() throws IOException { final A a = init.run(); try { return body.f(a).run(); } catch (final IOException e) { throw e; } finally { fin.f(a); } } }; } public static final IO unit(final A a) { return new IO() { @Override public A run() throws IOException { return a; } }; } /** * A function that feeds an iteratee with lines read from a {@link BufferedReader}. */ public static F, IO>>> lineReader() { final F, Boolean> isDone = new F, Boolean>() { final F>, P1> done = constant(P.p(true)); final F, IterV>, P1> cont = constant(P.p(false)); @Override public Boolean f(final IterV i) { return i.fold(done, cont)._1(); } }; return new F, IO>>>() { @Override public F, IO>> f(final BufferedReader r) { return new F, IO>>() { final F>, P1>> done = errorF("iteratee is done"); //$NON-NLS-1$ @Override public IO> f(final IterV it) { // use loop instead of recursion because of missing TCO return new IO>() { @Override public IterV run() throws IOException { IterV i = it; while (!isDone.f(i)) { final String s = r.readLine(); if (s == null) { return i; } final Input input = Input.el(s); final F, IterV>, P1>> cont = Function., IterV>apply(input).lazy(); i = i.fold(done, cont)._1(); } return i; } }; } }; } }; } /** * A function that feeds an iteratee with character chunks read from a {@link Reader} * (char[] of size {@link #DEFAULT_BUFFER_SIZE}). */ public static F, IO>>> charChunkReader() { final F, Boolean> isDone = new F, Boolean>() { final F>, P1> done = constant(P.p(true)); final F, IterV>, P1> cont = constant(P.p(false)); @Override public Boolean f(final IterV i) { return i.fold(done, cont)._1(); } }; return new F, IO>>>() { @Override public F, IO>> f(final Reader r) { return new F, IO>>() { final F>, P1>> done = errorF("iteratee is done"); //$NON-NLS-1$ @Override public IO> f(final IterV it) { // use loop instead of recursion because of missing TCO return new IO>() { @Override public IterV run() throws IOException { IterV i = it; while (!isDone.f(i)) { char[] buffer = new char[DEFAULT_BUFFER_SIZE]; final int numRead = r.read(buffer); if (numRead == -1) { return i; } if(numRead < buffer.length) { buffer = Arrays.copyOfRange(buffer, 0, numRead); } final Input input = Input.el(buffer); final F, IterV>, P1>> cont = Function., IterV>apply(input).lazy(); i = i.fold(done, cont)._1(); } return i; } }; } }; } }; } /** * A function that feeds an iteratee with characters read from a {@link Reader} * (chars are read in chunks of size {@link #DEFAULT_BUFFER_SIZE}). */ public static F, IO>>> charChunkReader2() { final F, Boolean> isDone = new F, Boolean>() { final F>, P1> done = constant(P.p(true)); final F, IterV>, P1> cont = constant(P.p(false)); @Override public Boolean f(final IterV i) { return i.fold(done, cont)._1(); } }; return new F, IO>>>() { @Override public F, IO>> f(final Reader r) { return new F, IO>>() { final F>, IterV> done = errorF("iteratee is done"); //$NON-NLS-1$ @Override public IO> f(final IterV it) { // use loop instead of recursion because of missing TCO return new IO>() { @Override public IterV run() throws IOException { IterV i = it; while (!isDone.f(i)) { char[] buffer = new char[DEFAULT_BUFFER_SIZE]; final int numRead = r.read(buffer); if (numRead == -1) { return i; } if(numRead < buffer.length) { buffer = Arrays.copyOfRange(buffer, 0, numRead); } for(int c = 0; c < buffer.length; c++) { final Input input = Input.el(buffer[c]); final F, IterV>, IterV> cont = Function., IterV>apply(input); i = i.fold(done, cont); } } return i; } }; } }; } }; } public abstract A run() throws IOException; public final IO map(final F f) { return new IO() { @Override public B run() throws IOException { return f.f(IO.this.run()); } }; } public final IO bind(final F> f) { return new IO() { @Override public B run() throws IOException { return f.f(IO.this.run()).run(); } }; } }