--- /dev/null
+package jrw.resp;
+
+import jrw.*;
+import jrw.sp.*;
+import java.util.*;
+
+public abstract class HtmlResponse extends Restart {
+ public final Skeleton skel = Skeleton.defskel.get().get();
+
+ public HtmlResponse(String title, Object... detail) {
+ skel.title(title);
+ skel.body(detail);
+ }
+}
--- /dev/null
+package jrw.resp;
+
+import jrw.*;
+import jrw.sp.*;
+import jrw.util.*;
+import java.util.*;
+
+public class HttpError extends UserError {
+ public int code;
+
+ public HttpError(int code, String title, Object detail) {
+ super(title, detail);
+ this.code = code;
+ }
+ public HttpError(int code, Object detail) {
+ this(code, Http.statusinfo.get(code).status, detail);
+ }
+ public HttpError(int code) {
+ this(code, Http.statusinfo.get(code).message);
+ }
+
+ public Map<Object, Object> handle(Request req) {
+ req.status(code + " " + skel.title);
+ return(super.handle(req));
+ }
+}
--- /dev/null
+package jrw.resp;
+
+import jrw.*;
+import jrw.sp.*;
+import java.util.*;
+
+public class Message extends HtmlResponse {
+ public Message(String title, Object... detail) {
+ super(title, detail);
+ }
+
+ public Map<Object, Object> handle(Request req) {
+ return(xhtml.response(req, skel.message(req)));
+ }
+}
--- /dev/null
+package jrw.resp;
+
+public class NotFound extends HttpError {
+ public NotFound() {
+ super(404);
+ }
+}
--- /dev/null
+package jrw.resp;
+
+import jrw.*;
+import jrw.sp.*;
+import java.util.*;
+import java.util.function.*;
+import static jrw.sp.cons.*;
+import static jrw.sp.xhtml.cons.*;
+
+public class Skeleton {
+ public static Environment.Variable<Supplier<? extends Skeleton>> defskel = new Environment.Variable<>(() -> Skeleton::new);
+ public List<String> styles = new ArrayList<>();
+ public Element body = xhtml.cons.body();
+ public String title;
+
+ public Skeleton(String title, Object... contents) {
+ this.title = title;
+ populate(body, contents);
+ }
+
+ public Skeleton() {
+ this("");
+ }
+
+ public Skeleton title(String title) {this.title = title; return(this);}
+ public Skeleton style(String... styles) {this.styles.addAll(Arrays.asList(styles)); return(this);}
+ public Skeleton body(Object... data) {populate(body, data); return(this);}
+
+ public Element head(Request req) {
+ Element head = xhtml.cons.head(xhtml.cons.title(title));
+ for(String style : styles)
+ populate(head, link($("rel", "stylesheet"), $("type", "text/css"), $("href", style)));
+ return(head);
+ }
+
+ public Element body(Request req) {
+ return(body);
+ }
+
+ public Element message(Request req) {
+ return(html(head(req), body(req)));
+ }
+
+ public Element error(Request req) {
+ return(message(req));
+ }
+}
--- /dev/null
+package jrw.resp;
+
+import jrw.*;
+import jrw.sp.*;
+import java.util.*;
+
+public class UserError extends HtmlResponse {
+ public UserError(String title, Object... detail) {
+ super(title, detail);
+ }
+
+ public Map<Object, Object> handle(Request req) {
+ return(xhtml.response(req, skel.error(req)));
+ }
+}
--- /dev/null
+package jrw.sp;
+
+public class DocType {
+ public final String rootname, pubid, dtdid;
+
+ public DocType(String rootname, String pubid, String dtdid) {
+ this.rootname = rootname;
+ this.pubid = pubid;
+ this.dtdid = dtdid;
+ }
+
+ public String format() {
+ return(String.format("<!DOCTYPE %s PUBLIC \"%s\" \"%s\">", rootname, pubid, dtdid));
+ }
+}
--- /dev/null
+package jrw.sp;
+
+import java.util.*;
+
+public class Element extends Node {
+ public final Name name;
+ public final List<Node> children = new ArrayList<>();
+ public final Map<Name, String> attribs = new HashMap<>();
+
+ public Element(Name name) {
+ this.name = name;
+ }
+
+ public Element add(Node ch) {
+ children.add(ch);
+ return(this);
+ }
+
+ public Element set(Name attrib, String val) {
+ attribs.put(attrib, val);
+ return(this);
+ }
+
+ public String toString() {
+ return(String.format("#<element %s %d attr %d ch>", name, attribs.size(), children.size()));
+ }
+}
--- /dev/null
+package jrw.sp;
+
+import jrw.util.*;
+import java.util.*;
+
+public class Formatter extends LazyPChannel {
+ private final Element root;
+ private final String header;
+ private final List<Frame> stack = new ArrayList<>();
+ private final Map<Namespace, String> ns = new IdentityHashMap<>();
+ private boolean headed = false;
+
+ class Frame {
+ Element el;
+ Iterator<Map.Entry<Name, String>> ai;
+ Iterator<Node> ci;
+ boolean sh;
+ boolean h, e, t;
+
+ Frame(Element el) {
+ this.el = el;
+ this.ai = el.attribs.entrySet().iterator();
+ this.ci = el.children.iterator();
+ this.sh = shorten(el);
+ }
+ }
+
+ private void countns(Map<Namespace, Integer> freq, Set<Namespace> attrs, Element el) {
+ for(Name anm : el.attribs.keySet()) {
+ if(anm.ns != null) {
+ attrs.add(anm.ns);
+ Integer f = freq.get(anm.ns);
+ freq.put(anm.ns, ((f == null) ? 0 : f) + 1);
+ }
+ }
+ Integer f = freq.get(el.name.ns);
+ freq.put(el.name.ns, ((f == null) ? 0 : f) + 1);
+ for(Node ch : el.children) {
+ if(ch instanceof Element)
+ countns(freq, attrs, (Element)ch);
+ }
+ }
+
+ private void calcnsnames() {
+ Map<Namespace, Integer> freq = new IdentityHashMap<>();
+ Set<Namespace> attrs = new HashSet<>();
+ countns(freq, attrs, root);
+ if(freq.get(null) != null) {
+ ns.put(null, null);
+ freq.remove(null);
+ } else if(!attrs.contains(root.name.ns)) {
+ ns.put(root.name.ns, null);
+ freq.remove(root.name.ns);
+ }
+ List<Namespace> order = new ArrayList<>(freq.keySet());
+ Collection<String> ass = new HashSet<>();
+ ass.add(null);
+ Collections.sort(order, (x, y) -> (freq.get(y) - freq.get(x)));
+ for(Namespace ns : order) {
+ String p = ns.prefabb;
+ if((p != null) && !ass.contains(p)) {
+ this.ns.put(ns, p);
+ ass.add(p);
+ } else {
+ int i;
+ if(p == null) {
+ p = "ns";
+ i = 1;
+ } else {
+ i = 2;
+ }
+ while(ass.contains(p + i))
+ i++;
+ this.ns.put(ns, p + i);
+ ass.add(p + i);
+ }
+ }
+ }
+
+ public Formatter(DocType doctype, Element root) {
+ this.header = "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n" + doctype.format() + "\n";
+ this.root = root;
+ calcnsnames();
+ Frame rf = new Frame(root);
+ Map<Name, String> ra = new HashMap<>(root.attribs);
+ for(Map.Entry<Namespace, String> ent : this.ns.entrySet()) {
+ Namespace ns = ent.getKey();
+ String abb = ent.getValue();
+ if(ns == null)
+ continue;
+ ra.put(new Name((abb == null) ? "xmlns" : ("xmlns:" + abb)), ns.uri);
+ }
+ rf.ai = ra.entrySet().iterator();
+ stack.add(rf);
+ }
+
+ private String fmtname(Name nm) {
+ String abb = ns.get(nm.ns);
+ return((abb == null) ? nm.local : (abb + ":" + nm.local));
+ }
+
+ private String head(Element el) {
+ return(String.format("<%s", fmtname(el.name)));
+ }
+
+ private String tail(Element el) {
+ return(String.format("</%s>", fmtname(el.name)));
+ }
+
+ private String attrquote(String val) {
+ char qc;
+ if(val.indexOf('"') >= 0) {
+ qc = '\'';
+ val = val.replace("'", "'");
+ } else {
+ qc = '"';
+ val = val.replace("\"", """);
+ }
+ val = val.replace("&", "&");
+ val = val.replace("<", "<");
+ val = val.replace(">", ">");
+ return(qc + val + qc);
+ }
+
+ private String attr(Name nm, String value) {
+ String anm = (nm.ns == null) ? nm.local : fmtname(nm);
+ return(String.format(" %s=%s", anm, attrquote(value)));
+ }
+
+ private String quote(String text) {
+ text = text.replace("&", "&");
+ text = text.replace("<", "<");
+ text = text.replace(">", ">");
+ return(text);
+ }
+
+ protected boolean shorten(Element el) {
+ return(el.children.isEmpty());
+ }
+
+ protected boolean produce() {
+ if(!headed) {
+ headed = true;
+ if(write(header))
+ return(false);
+ }
+ if(stack.isEmpty())
+ return(true);
+ Frame f = stack.get(stack.size() - 1);
+ if(!f.h && (f.h = true) && write(head(f.el)))
+ return(false);
+ while(f.ai.hasNext()) {
+ Map.Entry<Name, String> ent = f.ai.next();
+ if(write(attr(ent.getKey(), ent.getValue())))
+ return(false);
+ }
+ if(!f.sh) {
+ if(!f.e && (f.e = true) && write(">"))
+ return(false);
+ if(f.ci.hasNext()) {
+ Node ch = f.ci.next();
+ if(ch instanceof Text) {
+ write(quote(((Text)ch).text));
+ } else if(ch instanceof Raw) {
+ write(((Raw)ch).text);
+ } else {
+ stack.add(new Frame((Element)ch));
+ }
+ return(false);
+ }
+ if(!f.t && (f.t = true) && write(tail(f.el)))
+ return(false);
+ } else {
+ if(!f.e && (f.e = true) && write(" />"))
+ return(false);
+ }
+ stack.remove(stack.size() - 1);
+ return(false);
+ }
+}
--- /dev/null
+package jrw.sp;
+
+import java.util.*;
+
+public class HtmlFormatter extends Formatter {
+ private static final Collection<String> shortenable = new HashSet<>(Arrays.asList("audio", "br", "hr", "img", "input", "meta", "link", "source", "video"));
+
+ public HtmlFormatter(Element root) {
+ super(xhtml.doctype, root);
+ }
+
+ protected boolean shorten(Element el) {
+ if((el.name.ns == xhtml.ns) && !shortenable.contains(el.name.local))
+ return(false);
+ return(super.shorten(el));
+ }
+}
--- /dev/null
+package jrw.sp;
+
+public class Name {
+ public final Namespace ns;
+ public final String local;
+
+ public Name(Namespace ns, String local) {
+ if(local == null)
+ throw(new NullPointerException());
+ this.ns = ns;
+ this.local = local;
+ }
+
+ public Name(String local) {
+ this(null, local);
+ }
+
+ public int hashCode() {
+ return(System.identityHashCode(ns) + local.hashCode());
+ }
+
+ private boolean equals(Name that) {
+ return((this.ns == that.ns) && this.local.equals(that.local));
+ }
+
+ public boolean equals(Object x) {
+ return((x instanceof Name) && equals((Name)x));
+ }
+
+ public String toString() {
+ return((ns == null) ? local : (ns.prefabb + ":" + local));
+ }
+}
--- /dev/null
+package jrw.sp;
+
+public class Namespace {
+ public final String uri;
+ public final String prefabb;
+
+ public Namespace(String uri, String prefabb) {
+ this.uri = uri;
+ this.prefabb = prefabb;
+ }
+}
--- /dev/null
+package jrw.sp;
+
+public abstract class Node {
+ Node() {}
+}
--- /dev/null
+package jrw.sp;
+
+public interface Populous {
+ public void populate(Element el);
+}
--- /dev/null
+package jrw.sp;
+
+public class Raw extends Node {
+ public final String text;
+
+ public Raw(String text) {
+ this.text = text;
+ }
+}
--- /dev/null
+package jrw.sp;
+
+public class Text extends Node {
+ public final String text;
+
+ public Text(String text) {
+ this.text = text;
+ }
+}
--- /dev/null
+package jrw.sp;
+
+import java.util.*;
+
+public class cons {
+ public static class Attribute {
+ public final Name name;
+ public final String value;
+
+ public Attribute(Name name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+ }
+
+ public static Attribute $(Name name, String value) {
+ return(new Attribute(name, value));
+ }
+
+ public static Attribute $(String name, String value) {
+ return($(new Name(name), value));
+ }
+
+ public static Attribute $(Namespace ns, String local, String value) {
+ return($(new Name(ns, local), value));
+ }
+
+ private static void populate0(Element el, Iterable<?> contents) {
+ for(Object ob : contents) {
+ if(ob == null) {
+ } else if(ob instanceof Node) {
+ el.add((Node)ob);
+ } else if(ob instanceof Attribute) {
+ el.set(((Attribute)ob).name, ((Attribute)ob).value);
+ } else if(ob instanceof Populous) {
+ ((Populous)ob).populate(el);
+ } else if(ob instanceof Object[]) {
+ populate0(el, Arrays.asList((Object[])ob));
+ } else if(ob instanceof Iterable) {
+ populate0(el, (Iterable<?>)ob);
+ } else if(ob instanceof String) {
+ el.add(new Text((String)ob));
+ } else {
+ el.add(new Text(ob.toString()));
+ }
+ }
+ }
+
+ public static Element populate(Element el, Object... contents) {
+ populate0(el, Arrays.asList(contents));
+ return(el);
+ }
+}
--- /dev/null
+package jrw.sp;
+
+import jrw.*;
+import java.util.*;
+import static jrw.sp.cons.populate;
+
+public class xhtml {
+ public static final Namespace ns = new Namespace("http://www.w3.org/1999/xhtml", "h");
+ public static final DocType doctype = new DocType("html", "-//W3C//DTD XHTML 1.1//EN", "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd");
+
+ public static class cons {
+ private static final Name html = new Name(ns, "html");
+ public static Element html(Object... c) {return(populate(new Element(html), c));}
+ private static final Name head = new Name(ns, "head");
+ public static Element head(Object... c) {return(populate(new Element(head), c));}
+ private static final Name base = new Name(ns, "base");
+ public static Element base(Object... c) {return(populate(new Element(base), c));}
+ private static final Name title = new Name(ns, "title");
+ public static Element title(Object... c) {return(populate(new Element(title), c));}
+ private static final Name link = new Name(ns, "link");
+ public static Element link(Object... c) {return(populate(new Element(link), c));}
+ private static final Name meta = new Name(ns, "meta");
+ public static Element meta(Object... c) {return(populate(new Element(meta), c));}
+ private static final Name style = new Name(ns, "style");
+ public static Element style(Object... c) {return(populate(new Element(style), c));}
+ private static final Name script = new Name(ns, "script");
+ public static Element script(Object... c) {return(populate(new Element(script), c));}
+ private static final Name body = new Name(ns, "body");
+ public static Element body(Object... c) {return(populate(new Element(body), c));}
+ private static final Name div = new Name(ns, "div");
+ public static Element div(Object... c) {return(populate(new Element(div), c));}
+ private static final Name span = new Name(ns, "span");
+ public static Element span(Object... c) {return(populate(new Element(span), c));}
+ private static final Name p = new Name(ns, "p");
+ public static Element p(Object... c) {return(populate(new Element(p), c));}
+ private static final Name ul = new Name(ns, "ul");
+ public static Element ul(Object... c) {return(populate(new Element(ul), c));}
+ private static final Name ol = new Name(ns, "ol");
+ public static Element ol(Object... c) {return(populate(new Element(ol), c));}
+ private static final Name li = new Name(ns, "li");
+ public static Element li(Object... c) {return(populate(new Element(li), c));}
+ private static final Name dl = new Name(ns, "dl");
+ public static Element dl(Object... c) {return(populate(new Element(dl), c));}
+ private static final Name dt = new Name(ns, "dt");
+ public static Element dt(Object... c) {return(populate(new Element(dt), c));}
+ private static final Name dd = new Name(ns, "dd");
+ public static Element dd(Object... c) {return(populate(new Element(dd), c));}
+ private static final Name table = new Name(ns, "table");
+ public static Element table(Object... c) {return(populate(new Element(table), c));}
+ private static final Name th = new Name(ns, "th");
+ public static Element th(Object... c) {return(populate(new Element(th), c));}
+ private static final Name tr = new Name(ns, "tr");
+ public static Element tr(Object... c) {return(populate(new Element(tr), c));}
+ private static final Name td = new Name(ns, "td");
+ public static Element td(Object... c) {return(populate(new Element(td), c));}
+ private static final Name a = new Name(ns, "a");
+ public static Element a(Object... c) {return(populate(new Element(a), c));}
+ private static final Name img = new Name(ns, "img");
+ public static Element img(Object... c) {return(populate(new Element(img), c));}
+ private static final Name video = new Name(ns, "video");
+ public static Element video(Object... c) {return(populate(new Element(video), c));}
+ private static final Name audio = new Name(ns, "audio");
+ public static Element audio(Object... c) {return(populate(new Element(audio), c));}
+ private static final Name source = new Name(ns, "source");
+ public static Element source(Object... c) {return(populate(new Element(source), c));}
+ private static final Name track = new Name(ns, "track");
+ public static Element track(Object... c) {return(populate(new Element(track), c));}
+ private static final Name form = new Name(ns, "form");
+ public static Element form(Object... c) {return(populate(new Element(form), c));}
+ private static final Name input = new Name(ns, "input");
+ public static Element input(Object... c) {return(populate(new Element(input), c));}
+ private static final Name em = new Name(ns, "em");
+ public static Element em(Object... c) {return(populate(new Element(em), c));}
+ private static final Name strong = new Name(ns, "strong");
+ public static Element strong(Object... c) {return(populate(new Element(strong), c));}
+ private static final Name hr = new Name(ns, "hr");
+ public static Element hr(Object... c) {return(populate(new Element(hr), c));}
+ private static final Name br = new Name(ns, "br");
+ public static Element br(Object... c) {return(populate(new Element(br), c));}
+ private static final Name blockquote = new Name(ns, "blockquote");
+ public static Element blockquote(Object... c) {return(populate(new Element(blockquote), c));}
+ private static final Name code = new Name(ns, "code");
+ public static Element code(Object... c) {return(populate(new Element(code), c));}
+ private static final Name pre = new Name(ns, "pre");
+ public static Element pre(Object... c) {return(populate(new Element(pre), c));}
+ private static final Name h1 = new Name(ns, "h1");
+ public static Element h1(Object... c) {return(populate(new Element(h1), c));}
+ private static final Name h2 = new Name(ns, "h2");
+ public static Element h2(Object... c) {return(populate(new Element(h2), c));}
+ private static final Name h3 = new Name(ns, "h3");
+ public static Element h3(Object... c) {return(populate(new Element(h3), c));}
+ private static final Name h4 = new Name(ns, "h4");
+ public static Element h4(Object... c) {return(populate(new Element(h4), c));}
+ private static final Name h5 = new Name(ns, "h5");
+ public static Element h5(Object... c) {return(populate(new Element(h5), c));}
+ private static final Name h6 = new Name(ns, "h6");
+ public static Element h6(Object... c) {return(populate(new Element(h6), c));}
+ }
+
+ public static Map<Object, Object> response(Request req, Element root) {
+ // XXX: Use proper Content-Type for clients accepting it.
+ req.ohead("Content-Type", "text/html; charset=utf-8", true);
+ req.body(new HtmlFormatter(root));
+ return(req.response());
+ }
+}
package jrw.util;
+import java.util.*;
+
public class Http {
public static final java.nio.charset.Charset UTF8 = java.nio.charset.Charset.forName("UTF-8");
public static final java.nio.charset.Charset LATIN1 = java.nio.charset.Charset.forName("ISO-8859-1");
public static final java.nio.charset.Charset ASCII = java.nio.charset.Charset.forName("US-ASCII");
+ public static final Map<Integer, StatusInfo> statusinfo;
+
+ public static class StatusInfo {
+ public final String status, message;
+ public StatusInfo(String status, String message) {this.status = status; this.message = message;}
+ }
+
+ static {
+ Map<Integer, StatusInfo> buf = new HashMap<>();
+ buf.put(400, new StatusInfo("Bad Request", "Invalid HTTP request."));
+ buf.put(401, new StatusInfo("Unauthorized", "Authentication must be provided for the requested resource.."));
+ buf.put(403, new StatusInfo("Forbidden", "You ar enot authorized for the requested resource."));
+ buf.put(404, new StatusInfo("Not Found", "The requested resource was not found."));
+ buf.put(405, new StatusInfo("Method Not Allowed", "The request method is not valid or permitted by the requested resource."));
+ buf.put(429, new StatusInfo("Too Many Requests", "Your client is sending more frequent requests than are accepted."));
+ buf.put(500, new StatusInfo("Server Error", "An internal error occurred."));
+ buf.put(501, new StatusInfo("Not Implemented", "The requested functionality has not been implemented."));
+ buf.put(503, new StatusInfo("Service Unavailable", "Service is being denied at this time."));
+ statusinfo = Collections.unmodifiableMap(buf);
+ }
}
--- /dev/null
+package jrw.util;
+
+import jrw.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.nio.charset.*;
+
+public abstract class LazyPChannel implements ReadableByteChannel {
+ private ByteBuffer curbuf = null;
+ private boolean eof = false;
+ private CharsetEncoder enc = null;
+ private Runnable rem = null;
+
+ protected boolean write(byte[] data, int off, int len) {
+ if(rem != null) throw(new IllegalStateException("buffer filled"));
+ int t = Math.min(curbuf.remaining(), len);
+ curbuf.put(data, off, t);
+ if(len > t) {
+ rem = () -> write(data, off + t, len - t);
+ return(true);
+ }
+ return(false);
+ }
+ protected boolean write(byte[] data) {return(write(data, 0, data.length));}
+
+ protected boolean write(CharBuffer buf) {
+ if(rem != null) throw(new IllegalStateException("buffer filled"));
+ if(enc == null)
+ enc = charset().newEncoder();
+ while(true) {
+ int pp = buf.position();
+ CoderResult res = enc.encode(buf, curbuf, false);
+ if(buf.remaining() == 0)
+ return(false);
+ if(res.isUnderflow()) {
+ if(pp == buf.position()) {
+ /* XXX? Not sure if this can be expected to
+ * happen. I'm not aware of any charsets that should
+ * require it, and it would complicate the design
+ * significantly. */
+ throw(new RuntimeException("encoder not consuming input"));
+ }
+ } else if(res.isOverflow()) {
+ rem = () -> write(buf);
+ return(true);
+ } else {
+ try {
+ res.throwException();
+ } catch(CharacterCodingException e) {
+ throw(new RuntimeException(e));
+ }
+ }
+ }
+ }
+
+ protected boolean write(CharSequence chars) {
+ CharBuffer buf = (chars instanceof CharBuffer) ? ((CharBuffer)chars).duplicate() : CharBuffer.wrap(chars);
+ return(write(buf));
+ }
+
+ private void encflush2() {
+ while(true) {
+ CoderResult res = enc.flush(curbuf);
+ if(res.isOverflow()) {
+ rem = this::encflush1;
+ return;
+ } else if(res.isUnderflow()) {
+ return;
+ } else {
+ try {
+ res.throwException();
+ } catch(CharacterCodingException e) {
+ throw(new RuntimeException(e));
+ }
+ }
+ }
+ }
+
+ private void encflush1() {
+ CharBuffer empty = CharBuffer.wrap("");
+ while(true) {
+ CoderResult res = enc.encode(empty, curbuf, true);
+ if(res.isOverflow()) {
+ rem = this::encflush1;
+ return;
+ } else if(res.isUnderflow()) {
+ rem = this::encflush2;
+ return;
+ } else {
+ try {
+ res.throwException();
+ } catch(CharacterCodingException e) {
+ throw(new RuntimeException(e));
+ }
+ }
+ }
+ }
+
+ private void encflush() {
+ if(enc != null)
+ rem = this::encflush1;
+ }
+
+ protected Charset charset() {return(Http.UTF8);}
+
+ protected abstract boolean produce();
+
+ public int read(ByteBuffer buf) {
+ curbuf = buf;
+ try {
+ int op = buf.position();
+ while(buf.remaining() > 0) {
+ Runnable rem = this.rem;
+ this.rem = null;
+ if(rem != null) {
+ rem.run();
+ } else {
+ if(eof) {
+ break;
+ } else if(produce()) {
+ encflush();
+ eof = true;
+ }
+ }
+ }
+ if(eof && (buf.position() == op))
+ return(-1);
+ return(buf.position() - op);
+ } finally {
+ curbuf = null;
+ }
+ }
+
+ public void close() {}
+ public boolean isOpen() {return(true);}
+}