Added basic HTML generation and response handling.
authorFredrik Tolf <fredrik@dolda2000.com>
Sat, 5 Mar 2022 13:17:02 +0000 (14:17 +0100)
committerFredrik Tolf <fredrik@dolda2000.com>
Sat, 5 Mar 2022 13:17:02 +0000 (14:17 +0100)
20 files changed:
src/jrw/resp/HtmlResponse.java [new file with mode: 0644]
src/jrw/resp/HttpError.java [new file with mode: 0644]
src/jrw/resp/Message.java [new file with mode: 0644]
src/jrw/resp/NotFound.java [new file with mode: 0644]
src/jrw/resp/Skeleton.java [new file with mode: 0644]
src/jrw/resp/UserError.java [new file with mode: 0644]
src/jrw/sp/DocType.java [new file with mode: 0644]
src/jrw/sp/Element.java [new file with mode: 0644]
src/jrw/sp/Formatter.java [new file with mode: 0644]
src/jrw/sp/HtmlFormatter.java [new file with mode: 0644]
src/jrw/sp/Name.java [new file with mode: 0644]
src/jrw/sp/Namespace.java [new file with mode: 0644]
src/jrw/sp/Node.java [new file with mode: 0644]
src/jrw/sp/Populous.java [new file with mode: 0644]
src/jrw/sp/Raw.java [new file with mode: 0644]
src/jrw/sp/Text.java [new file with mode: 0644]
src/jrw/sp/cons.java [new file with mode: 0644]
src/jrw/sp/xhtml.java [new file with mode: 0644]
src/jrw/util/Http.java
src/jrw/util/LazyPChannel.java [new file with mode: 0644]

diff --git a/src/jrw/resp/HtmlResponse.java b/src/jrw/resp/HtmlResponse.java
new file mode 100644 (file)
index 0000000..43ae12c
--- /dev/null
@@ -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 (file)
index 0000000..0593f5a
--- /dev/null
@@ -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<Object, Object> 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 (file)
index 0000000..b61eb7a
--- /dev/null
@@ -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<Object, Object> 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 (file)
index 0000000..a81b188
--- /dev/null
@@ -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 (file)
index 0000000..75655bc
--- /dev/null
@@ -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<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));
+    }
+}
diff --git a/src/jrw/resp/UserError.java b/src/jrw/resp/UserError.java
new file mode 100644 (file)
index 0000000..4cbf00b
--- /dev/null
@@ -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<Object, Object> 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 (file)
index 0000000..f6750ec
--- /dev/null
@@ -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("<!DOCTYPE %s PUBLIC \"%s\" \"%s\">", rootname, pubid, dtdid));
+    }
+}
diff --git a/src/jrw/sp/Element.java b/src/jrw/sp/Element.java
new file mode 100644 (file)
index 0000000..e33e6a3
--- /dev/null
@@ -0,0 +1,27 @@
+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()));
+    }
+}
diff --git a/src/jrw/sp/Formatter.java b/src/jrw/sp/Formatter.java
new file mode 100644 (file)
index 0000000..0791614
--- /dev/null
@@ -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<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("'", "&apos;");
+       } else {
+           qc = '"';
+           val = val.replace("\"", "&quot;");
+       }
+       val = val.replace("&", "&amp;");
+       val = val.replace("<", "&lt;");
+       val = val.replace(">", "&gt;");
+       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("&", "&amp;");
+       text = text.replace("<", "&lt;");
+       text = text.replace(">", "&gt;");
+       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);
+    }
+}
diff --git a/src/jrw/sp/HtmlFormatter.java b/src/jrw/sp/HtmlFormatter.java
new file mode 100644 (file)
index 0000000..c7a4e5a
--- /dev/null
@@ -0,0 +1,17 @@
+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));
+    }
+}
diff --git a/src/jrw/sp/Name.java b/src/jrw/sp/Name.java
new file mode 100644 (file)
index 0000000..9029b7a
--- /dev/null
@@ -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 (file)
index 0000000..07b2dae
--- /dev/null
@@ -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 (file)
index 0000000..0313d82
--- /dev/null
@@ -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 (file)
index 0000000..2a58b7a
--- /dev/null
@@ -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 (file)
index 0000000..e512711
--- /dev/null
@@ -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 (file)
index 0000000..8e93829
--- /dev/null
@@ -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 (file)
index 0000000..8c54288
--- /dev/null
@@ -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 (file)
index 0000000..6d9120c
--- /dev/null
@@ -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<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());
+    }
+}
index a5843d5..cadd856 100644 (file)
@@ -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<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);
+    }
 }
diff --git a/src/jrw/util/LazyPChannel.java b/src/jrw/util/LazyPChannel.java
new file mode 100644 (file)
index 0000000..2220391
--- /dev/null
@@ -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);}
+}