From: Fredrik Tolf Date: Sat, 5 Mar 2022 13:17:02 +0000 (+0100) Subject: Added basic HTML generation and response handling. X-Git-Url: http://git.dolda2000.com/gitweb/?a=commitdiff_plain;h=6e0043cc3f99a31bac74d6d0c7399e4c0b60d3fb;p=jrw.git Added basic HTML generation and response handling. --- diff --git a/src/jrw/resp/HtmlResponse.java b/src/jrw/resp/HtmlResponse.java new file mode 100644 index 0000000..43ae12c --- /dev/null +++ b/src/jrw/resp/HtmlResponse.java @@ -0,0 +1,14 @@ +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); + } +} diff --git a/src/jrw/resp/HttpError.java b/src/jrw/resp/HttpError.java new file mode 100644 index 0000000..0593f5a --- /dev/null +++ b/src/jrw/resp/HttpError.java @@ -0,0 +1,26 @@ +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 handle(Request req) { + req.status(code + " " + skel.title); + return(super.handle(req)); + } +} diff --git a/src/jrw/resp/Message.java b/src/jrw/resp/Message.java new file mode 100644 index 0000000..b61eb7a --- /dev/null +++ b/src/jrw/resp/Message.java @@ -0,0 +1,15 @@ +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 handle(Request req) { + return(xhtml.response(req, skel.message(req))); + } +} diff --git a/src/jrw/resp/NotFound.java b/src/jrw/resp/NotFound.java new file mode 100644 index 0000000..a81b188 --- /dev/null +++ b/src/jrw/resp/NotFound.java @@ -0,0 +1,7 @@ +package jrw.resp; + +public class NotFound extends HttpError { + public NotFound() { + super(404); + } +} diff --git a/src/jrw/resp/Skeleton.java b/src/jrw/resp/Skeleton.java new file mode 100644 index 0000000..75655bc --- /dev/null +++ b/src/jrw/resp/Skeleton.java @@ -0,0 +1,47 @@ +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> defskel = new Environment.Variable<>(() -> Skeleton::new); + public List 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)); + } +} diff --git a/src/jrw/resp/UserError.java b/src/jrw/resp/UserError.java new file mode 100644 index 0000000..4cbf00b --- /dev/null +++ b/src/jrw/resp/UserError.java @@ -0,0 +1,15 @@ +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 handle(Request req) { + return(xhtml.response(req, skel.error(req))); + } +} diff --git a/src/jrw/sp/DocType.java b/src/jrw/sp/DocType.java new file mode 100644 index 0000000..f6750ec --- /dev/null +++ b/src/jrw/sp/DocType.java @@ -0,0 +1,15 @@ +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("", rootname, pubid, dtdid)); + } +} diff --git a/src/jrw/sp/Element.java b/src/jrw/sp/Element.java new file mode 100644 index 0000000..e33e6a3 --- /dev/null +++ b/src/jrw/sp/Element.java @@ -0,0 +1,27 @@ +package jrw.sp; + +import java.util.*; + +public class Element extends Node { + public final Name name; + public final List children = new ArrayList<>(); + public final Map 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("#", name, attribs.size(), children.size())); + } +} diff --git a/src/jrw/sp/Formatter.java b/src/jrw/sp/Formatter.java new file mode 100644 index 0000000..0791614 --- /dev/null +++ b/src/jrw/sp/Formatter.java @@ -0,0 +1,180 @@ +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 stack = new ArrayList<>(); + private final Map ns = new IdentityHashMap<>(); + private boolean headed = false; + + class Frame { + Element el; + Iterator> ai; + Iterator 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 freq, Set 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 freq = new IdentityHashMap<>(); + Set 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 order = new ArrayList<>(freq.keySet()); + Collection 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 = "\n" + doctype.format() + "\n"; + this.root = root; + calcnsnames(); + Frame rf = new Frame(root); + Map ra = new HashMap<>(root.attribs); + for(Map.Entry 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("", 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 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); + } +} diff --git a/src/jrw/sp/HtmlFormatter.java b/src/jrw/sp/HtmlFormatter.java new file mode 100644 index 0000000..c7a4e5a --- /dev/null +++ b/src/jrw/sp/HtmlFormatter.java @@ -0,0 +1,17 @@ +package jrw.sp; + +import java.util.*; + +public class HtmlFormatter extends Formatter { + private static final Collection 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)); + } +} diff --git a/src/jrw/sp/Name.java b/src/jrw/sp/Name.java new file mode 100644 index 0000000..9029b7a --- /dev/null +++ b/src/jrw/sp/Name.java @@ -0,0 +1,33 @@ +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)); + } +} diff --git a/src/jrw/sp/Namespace.java b/src/jrw/sp/Namespace.java new file mode 100644 index 0000000..07b2dae --- /dev/null +++ b/src/jrw/sp/Namespace.java @@ -0,0 +1,11 @@ +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; + } +} diff --git a/src/jrw/sp/Node.java b/src/jrw/sp/Node.java new file mode 100644 index 0000000..0313d82 --- /dev/null +++ b/src/jrw/sp/Node.java @@ -0,0 +1,5 @@ +package jrw.sp; + +public abstract class Node { + Node() {} +} diff --git a/src/jrw/sp/Populous.java b/src/jrw/sp/Populous.java new file mode 100644 index 0000000..2a58b7a --- /dev/null +++ b/src/jrw/sp/Populous.java @@ -0,0 +1,5 @@ +package jrw.sp; + +public interface Populous { + public void populate(Element el); +} diff --git a/src/jrw/sp/Raw.java b/src/jrw/sp/Raw.java new file mode 100644 index 0000000..e512711 --- /dev/null +++ b/src/jrw/sp/Raw.java @@ -0,0 +1,9 @@ +package jrw.sp; + +public class Raw extends Node { + public final String text; + + public Raw(String text) { + this.text = text; + } +} diff --git a/src/jrw/sp/Text.java b/src/jrw/sp/Text.java new file mode 100644 index 0000000..8e93829 --- /dev/null +++ b/src/jrw/sp/Text.java @@ -0,0 +1,9 @@ +package jrw.sp; + +public class Text extends Node { + public final String text; + + public Text(String text) { + this.text = text; + } +} diff --git a/src/jrw/sp/cons.java b/src/jrw/sp/cons.java new file mode 100644 index 0000000..8c54288 --- /dev/null +++ b/src/jrw/sp/cons.java @@ -0,0 +1,53 @@ +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); + } +} diff --git a/src/jrw/sp/xhtml.java b/src/jrw/sp/xhtml.java new file mode 100644 index 0000000..6d9120c --- /dev/null +++ b/src/jrw/sp/xhtml.java @@ -0,0 +1,106 @@ +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 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()); + } +} diff --git a/src/jrw/util/Http.java b/src/jrw/util/Http.java index a5843d5..cadd856 100644 --- a/src/jrw/util/Http.java +++ b/src/jrw/util/Http.java @@ -1,7 +1,29 @@ 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 statusinfo; + + public static class StatusInfo { + public final String status, message; + public StatusInfo(String status, String message) {this.status = status; this.message = message;} + } + + static { + Map 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); + } } diff --git a/src/jrw/util/LazyPChannel.java b/src/jrw/util/LazyPChannel.java new file mode 100644 index 0000000..2220391 --- /dev/null +++ b/src/jrw/util/LazyPChannel.java @@ -0,0 +1,136 @@ +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);} +}