java/ch/wlkl/wsh/Cat.java

package ch.wlkl.wsh;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Cat creates {@link OpenClose} from a specification or manages a list of {@link OpenClose} for concatenated reading or writing:
 * <ul>
 * <li>{@link #make(String, Object)} creates an {@link OpenClose} from a specification
 * <li> an instance of Cat contains a list of {@link OpenClose}. When opened for read, it reads one after the other. When opened for writes may be interpersed with addIO. open and close options may vary for each member in the list.
 * </ul>
 * 
 * @author walter keller
 *
 */
public class Cat<T> extends Top implements Read<T>, Write<T> {
    public final static char eNoOpenClose = '-';
    public final static char eContinue = '+';
    public final static char eObject = '?';
    public final static char eExtFD = '&';
    public final static String eExtFDStr = String.valueOf(eExtFD);
    public final static char eBufVar = '#';

    private ArrayList<OpenClose> ios = null;
    private ArrayList<String> opts = null;
    private Env<?, ?> lazyClose = null;

    private Read<T> reader = null;
    int readIx = -9;
    private Buf<T> writer = null;

    public Cat () {
    }

    /**
     * return a new Cat as the concatenation of args, using {@link #reset(Object[])}
     */
    public Cat (Object... args) {
        this();
        reset(args);
    }

    /**
     * return a newly created OpenClose derived from argument String fn.
     * <ul>
     * <li> if opt starts with # {@link #eBufVar} and obj is not defined in the variable pool, return a new {@link Buf} after putting in the variable pool and opening it with argument opt.
     * <li> if opt starts with # {@link #eBufVar} and obj is defined in the variable pool, return its value from the variable pool after opening it with argument opt.
     * <li> if opt starts with &amp; {@link #eExtFD} return an external FD reader/writer using {@link ExtFD#ExtFD(Object[])}.
     * <li> otherwise if opt is a String return a {@link FileRW} using {@link FileRW#FileRW(String)}rn
     * <li> otherwise if opt contains a ? {@link #eObject} return
     * <ul><li>obj, if obj is instanceof {@link OpenClose}.</ul>
     * <li>{@link Buf#Buf(Collection)}, if obj is instanceof {@link Collection}.
     * <li> otherwise return a {@link FileRW} using {@link FileRW#FileRW(String)}.
     * </ul>
     */

    @SuppressWarnings("unchecked")
    public static OpenClose make(String opt, Object obj) {
        String oo = opt == null || opt.length() < 1 ? " " : opt; 
        String fn;
        if (oo.indexOf(eBufVar) >= 0) {
            return Env.env().hasKey(obj) ? make(oo.replace(eBufVar, eObject), Env.env().via(obj)) : Env.env().put(obj, new Buf<String>());
        } else if (oo.indexOf(eExtFD) >= 0) {
            return new ExtFD(opt, obj);
        } else  if (oo.indexOf(eObject) >= 0) {
            if (obj instanceof OpenClose)
                return (OpenClose) obj;
            else if (obj instanceof Collection)
                return new Buf<Object>((Collection<Object>) obj);
        } else if ((obj instanceof String) && null != (fn = (String) obj)) {
            return new FileRW(fn);
        }
        sFail("make(" + opt + ", " + obj + ") not implemented");
        return null;
    }

    public static String normalizeOpt(char fun, Object arg) {
        if (arg == null)
            return String.valueOf(fun);
        else if (! (arg instanceof String))
            sFail("option not a String: " + arg);
        String opt = (String) arg;
        if (opt.trim().length() < 1)
            return String.valueOf(fun);
        opt = opt.startsWith(">>") ? "a" + opt.substring(2) : opt.charAt(0) == '>' ? "w" + opt.substring(1) : opt.charAt(0) == '<' ? "r" + opt.substring(1) : "rwa ".indexOf(opt.charAt(0)) < 0 ? " " + opt : opt;
        if (fun == ' ' || fun == opt.charAt(0))
            return opt;
        else if (opt.charAt(0) == ' ')
            return "" + fun + opt.substring(1);
        else if (fun == ' ' || ("wa".indexOf(fun) >= 0 && "wa".indexOf(opt.charAt(0)) >= 0 ))
                return opt;
        else 
            sFail("option mismatch Cat.open(" + fun + ", " + opt + ")");
        return null;
    }

    /**
     * open oc with option opt, if opt does not contain a - ({@link #eNoOpenClose}
     * @param <T>
     * @param opt
     * @param oc
     * @return oc
     */
    public static <T> OpenClose open(char fun, String opt, OpenClose oc) {
        opt = normalizeOpt(fun, opt);
        if (opt.indexOf(eNoOpenClose) < 0) {
            oc.open(opt.substring(0, 1));
        }
        return oc;
    }
    /**
     * @return a Read&lt;String&gt; using {@link #make(String, Object)} and {@link #open(String) }
     */
    @SuppressWarnings("unchecked")
    public static Read<String> makeRead(String opt, Object o) {
        return (Read<String>) open('r', opt, make(opt, o));
    }
    
    public static <W> void writeAll(Write<W> w, String opt, Read<W> r) {
        Cat.open('r', opt, r);
        W one;
        while (null != (one = r.read()))
            w.write(one);
        if (opt.indexOf(Cat.eNoOpenClose) < 0)
            r.close();            
    }


    /**
     * reset the receiver, initialise the list with args using {@link #writeAll(String, Read)}
     */
    public void reset(Object... args) {
        close();
        if (opts != null) {
            opts.clear();
            ios.clear();
        }
        addIO(args);
    }

    public void writeAll(String opt, Read<T> r) {
        if (ios == null ) {
            ios = new ArrayList<OpenClose>();
            opts = new ArrayList<String> ();
        }
        if (writer != null) {
            writer.close();
            ios.add(writer);
            opts.add(" ");
            writer = null;
        }
        opts.add(opt);
        ios.add(r);
    }
    
    /**
     * add further pairs of option, specification to the lists of this Cat.
     * <br>if an element of args is an option ({@link Option#single(Object)} use it as current option ohterwise add the current option and element as specifiction. Start with current option empty.
     * Option and specification pairs are enventually passed to {@link #make(String, Object)}
     */
    @SuppressWarnings("unchecked")
    public void addIO(Object... args) {
        String opt = " ";
        String o1 = " ";
        for (Object obj : args) {
            if (null != (o1 = Option.single(obj))) {
                opt = normalizeOpt('r', o1);
            } else {
                writeAll(opt, (Read<T>) make(opt, obj));
            }
        }
    }
    
    public boolean lazyClose(Env<?, ?> lazy) {
        if (lazyClose != null)
            fail("lazyClose not null but " + lazyClose);
        if (ios == null)
            return false;
        lazyClose = lazy;
        return true;
    }

    /**
     * open the receiver. For
     * <br><ul>
     * <li>read the list of the receiver may specify any number of readers (including 0)
     * <li>write the list of the receiver at least one object. Only the first is used
     * </ul>
     * @param opt the readers or writers arg opened with this option (as first character).
     */
    @SuppressWarnings("unchecked")
    public void open(String opt) {
        close();
        if ("r".equals(opt)) {
            readIx = -1;
            if (ios != null)
                nextReader();
            else if (writer != null)
                (reader = (Read<T>) writer).open("r");
        } else if ("w".equals(opt) || "a".equals(opt)) {
            readIx = -7;
            if ("w".equals(opt) && ios != null) {
                    ios.clear();
                    opts.clear();
                }
            if (writer != null)
                writer.open(opt);
        } else {
            fail("open(" + opt + ") with bad opt");
        }
    }

    public void close() {
        Env<?, ?> toClose = null;
        if (reader != null && (opts == null || opts.get(readIx).indexOf(eNoOpenClose) < 0))
            reader.close();
        reader = null;
        if (readIx >= -1 && lazyClose != null) {
            toClose = lazyClose;
            lazyClose = null;
        }
            
        if (writer != null) {
            writer.close();
            if (ios != null) {
                ios.add(writer);
                opts.add(" ");
                writer = null;
            }
        }
        readIx = -9;
        if (toClose != null)
            toClose.close();
    }

    /**
     * open the concatenation, if args is nonEmpty they define the concatentation of readers. 
     * @param line
     */
    public void write(T line) {
        if (readIx != -7)
            fail("write but not opened for write");
        if (writer == null) 
            (writer = new Buf<T>()).open("w");            
        writer.write(line);
    }

    /**
     * read and return the next line.
     * at the end of the current reader try the next one. return null only at end of all readers.
     * fail (currently with null exception, but unspecified how) if not opened for read.
     */
    public T read() {
        T line;
        if (readIx < -1)
            fail("read but not opened for read");
        while (true) {
            if (reader != null && null != (line = reader.read())) 
                return line;
            if (! nextReader())
                return null;
        }
    }

    /**
     * close the current reader and open the next one
     * @return false after last reader otherwise true
     */
    @SuppressWarnings("unchecked")
    public boolean nextReader() {
        if (readIx < -1)
            fail("nextReader but not opened for read");
        if (reader != null && (opts == null || opts.get(readIx).indexOf(eNoOpenClose) < 0))
            reader.close();
        reader = null;
        if (ios == null || ++readIx >= ios.size())
            return false;
        reader = (Read<T>) open('r', opts.get(readIx), ios.get(readIx));
        return true;
    }    
}