Initial commit.
authorFredrik Tolf <fredrik@dolda2000.com>
Fri, 18 Feb 2022 14:50:08 +0000 (15:50 +0100)
committerFredrik Tolf <fredrik@dolda2000.com>
Fri, 18 Feb 2022 14:51:05 +0000 (15:51 +0100)
.gitignore [new file with mode: 0644]
build.xml [new file with mode: 0644]
src/jrw/Environment.java [new file with mode: 0644]
src/jrw/FormData.java [new file with mode: 0644]
src/jrw/Handler.java [new file with mode: 0644]
src/jrw/Http.java [new file with mode: 0644]
src/jrw/JagiWrapper.java [new file with mode: 0644]
src/jrw/Request.java [new file with mode: 0644]
src/jrw/Restart.java [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..eb35cbf
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<project name="jrw" default="jar">
+  
+  <property environment="env" />
+  
+  <target name="build-env">
+    <mkdir dir="build" />
+    <mkdir dir="build/bin" />
+  </target>
+
+  <target name="classes" depends="build-env">
+    <javac srcdir="src" destdir="build/bin" debug="on"
+          source="1.8" target="1.8" includeantruntime="no">
+      <compilerarg value="-Xlint:unchecked" />
+    </javac>
+  </target>
+  
+  <target name="jar" depends="build-env, classes">
+    <jar destfile="build/jrw.jar" basedir="build/bin" />
+  </target>
+  
+  <target name="clean">
+    <delete dir="build" />
+  </target>
+</project>
diff --git a/src/jrw/Environment.java b/src/jrw/Environment.java
new file mode 100644 (file)
index 0000000..f0f1347
--- /dev/null
@@ -0,0 +1,15 @@
+package jrw;
+
+import java.util.*;
+
+public class Environment {
+    public static Map<Object, Object> dispatch(Handler handler, Request req) {
+       while(true) {
+           try {
+               return(handler.handle(req));
+           } catch(Restart r) {
+               handler = r;
+           }
+       }
+    }
+}
diff --git a/src/jrw/FormData.java b/src/jrw/FormData.java
new file mode 100644 (file)
index 0000000..267aa6f
--- /dev/null
@@ -0,0 +1,169 @@
+package jrw;
+
+import java.util.*;
+import java.util.function.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+public class FormData extends HashMap<String, String> {
+    public static final int MAX_LENGTH = 1 << 20;
+
+    private static int htoi(byte hex) {
+       if((hex >= '0') && (hex <= '9'))
+           return(hex - '0');
+       if((hex >= 'A') && (hex <= 'F'))
+           return(hex - 'A' + 10);
+       if((hex >= 'a') && (hex <= 'f'))
+           return(hex - 'a' + 10);
+       return(0);
+    }
+
+    private static ByteBuffer unquoteb(ByteBuffer part) {
+       ByteBuffer ret = ByteBuffer.allocate(part.remaining());
+       while(part.remaining() > 0) {
+           int b = part.get() & 0xff;
+           if((b == '%') && (part.remaining() >= 2)) {
+               int n1 = htoi(part.get()), n2 = htoi(part.get());
+               ret.put((byte)((n1 << 4) | n2));
+           } else {
+               ret.put((byte)b);
+           }
+       }
+       ret.flip();
+       return(ret);
+    }
+
+    private static String unquote(ByteBuffer part) {
+       ByteBuffer dec = unquoteb(part);
+       try {
+           return(Http.UTF8.newDecoder().decode(dec.duplicate()).toString());
+       } catch(java.nio.charset.CharacterCodingException e) {
+           return(Http.LATIN1.decode(dec).toString());
+       }
+    }
+
+    private static String unquote(String part) {
+       if(part.indexOf('%') < 0)
+           return(part);
+       return(unquote(Http.UTF8.encode(CharBuffer.wrap(part))));
+    }
+
+    public static void parse(Map<? super String, ? super String> buf, ByteBuffer data) {
+       int p = data.position(), p2, p3;
+       while(p < data.limit()) {
+           for(p2 = p; (p2 < data.limit()) && (data.get(p2) != '&'); p2++);
+           for(p3 = p; (p3 < p2) && (data.get(p3) != '='); p3++);
+           if(p3 < p2) {
+               buf.put(unquote((ByteBuffer)data.duplicate().position(p).limit(p3)),
+                       unquote((ByteBuffer)data.duplicate().position(p3 + 1).limit(p2)));
+           }
+           p = p2 + 1;
+       }
+    }
+
+    public static void parse(Map<? super String, ? super String> buf, String data) {
+       int p = 0;
+       while(true) {
+           int p2 = data.indexOf('&', p);
+           String part = (p2 < 0) ? data.substring(p) : data.substring(p, p2);
+           int p3 = part.indexOf('=');
+           if(p3 >= 0)
+               buf.put(unquote(part.substring(0, p3)), unquote(part.substring(p3 + 1)));
+           if(p2 < 0)
+               break;
+           p = p2 + 1;
+       }
+    }
+
+    public static FormData read(Request req) {
+       FormData ret = new FormData();
+       String query = (String)req.env.get("QUERY_STRING");
+       if(query != null)
+           parse(ret, query);
+       if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
+           int max = MAX_LENGTH;
+           String clen = req.ihead("Content-Length", null);
+           if(clen != null) {
+               try {
+                   max = Math.min(max, Integer.parseInt(clen));
+               } catch(NumberFormatException e) {
+               }
+           }
+           ReadableByteChannel in = (ReadableByteChannel)req.env.get("jagi.input");
+           if(in instanceof SelectableChannel) {
+               try {
+                   ((SelectableChannel)in).configureBlocking(true);
+               } catch(IOException e) {
+               }
+           }
+           ByteBuffer buf = ByteBuffer.allocate(65536);
+           while(buf.position() < max) {
+               if(buf.remaining() == 0) {
+                   ByteBuffer n = ByteBuffer.allocate(Math.min(buf.capacity() * 2, max));
+                   buf.flip();
+                   n.put(buf);
+                   buf = n;
+               }
+               try {
+                   int rv = in.read(buf);
+                   if(rv <= 0)
+                       break;
+               } catch(IOException e) {
+                   break;
+               }
+           }
+           buf.flip();
+           parse(ret, buf);
+       }
+       return(ret);
+    }
+
+    public static FormData get(Request req) {
+       FormData ret = (FormData)req.env.get(FormData.class);
+       if(ret == null)
+           req.env.put(FormData.class, ret = read(req));
+       return(ret);
+    }
+
+    static class Collector extends ByteArrayOutputStream {
+       final FormData form = new FormData();
+       final Request req;
+
+       Collector(Request req) {
+           this.req = req;
+           req.env.put(FormData.class, this.form);
+           String query = (String)req.env.get("QUERY_STRING");
+           if(query != null)
+               parse(form, query);
+       }
+
+       public void write(int b) {
+           if(count < MAX_LENGTH)
+               super.write(b);
+       }
+
+       public void write(byte[] buf, int off, int len) {
+           len = Math.min(len, MAX_LENGTH - count);
+           if(len > 0)
+               super.write(buf, off, len);
+       }
+
+       public void close() {
+       }
+    }
+
+    public static Map<Object, Object> feed(Request req, Handler next) {
+       Map<Object, Object> resp = new HashMap<>();
+       if(req.ihead("Content-Type", "").equals("application/x-www-form-urlencoded")) {
+           resp.put("jagi.status", "feed-input");
+           resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
+           resp.put("jagi.input-sink", new Collector(req));
+       } else {
+           read(req);
+           resp.put("jagi.status", "chain");
+           resp.put("jagi.next", (Function<Map<Object, Object>, Map<Object, Object>>)env -> Environment.dispatch(next, req));
+       }
+       return(resp);
+    }
+}
diff --git a/src/jrw/Handler.java b/src/jrw/Handler.java
new file mode 100644 (file)
index 0000000..2c5f979
--- /dev/null
@@ -0,0 +1,7 @@
+package jrw;
+
+import java.util.*;
+
+public interface Handler {
+    public Map<Object, Object> handle(Request req);
+}
diff --git a/src/jrw/Http.java b/src/jrw/Http.java
new file mode 100644 (file)
index 0000000..f71b062
--- /dev/null
@@ -0,0 +1,7 @@
+package jrw;
+
+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");
+}
diff --git a/src/jrw/JagiWrapper.java b/src/jrw/JagiWrapper.java
new file mode 100644 (file)
index 0000000..8b1a3fb
--- /dev/null
@@ -0,0 +1,18 @@
+package jrw;
+
+import java.util.*;
+import java.util.function.*;
+
+public abstract class JagiWrapper implements Handler, Function<Map<Object, Object>, Map<Object, Object>> {
+    public Map<Object, Object> apply(Map<Object, Object> env) {
+       return(Environment.dispatch(this, new Request(env)));
+    }
+
+    public static JagiWrapper of(Handler handler) {
+       return(new JagiWrapper() {
+               public Map<Object, Object> handle(Request req) {
+                   return(handler.handle(req));
+               }
+           });
+    }
+}
diff --git a/src/jrw/Request.java b/src/jrw/Request.java
new file mode 100644 (file)
index 0000000..abe7161
--- /dev/null
@@ -0,0 +1,50 @@
+package jrw;
+
+import java.util.*;
+
+public class Request {
+    public final Map<Object, Object> env;
+    public final Map<Object, Object> resp = new HashMap<>();
+
+    public Request(Map<Object, Object> env) {
+       this.env = env;
+    }
+
+    public String ihead(String name, String def) {
+       StringBuilder buf = new StringBuilder();
+       buf.append("HTTP_");
+       for(int i = 0; i < name.length(); i++) {
+           char c = name.charAt(i);
+           if(c == '-')
+               buf.append('_');
+           else if((c >= 'a') && (c <= 'z'))
+               buf.append((char)(c + ('A' - 'a')));
+           else
+               buf.append(c);
+       }
+       Object ret = env.get(buf.toString());
+       if(ret instanceof String)
+           return((String)ret);
+       return(def);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void ohead(String name, Object val, boolean repl) {
+       name = "http." + name;
+       if(repl) {
+           resp.put(name, val);
+       } else {
+           Object cur = resp.get(name);
+           if(cur == null)
+               resp.put(name, val);
+           else if(cur instanceof Collection)
+               ((Collection)cur).add(val);
+           else
+               resp.put(name, new ArrayList<Object>(Arrays.asList(cur, val)));
+       }
+    }
+
+    public Map<Object, Object> response() {
+       return(resp);
+    }
+}
diff --git a/src/jrw/Restart.java b/src/jrw/Restart.java
new file mode 100644 (file)
index 0000000..de11218
--- /dev/null
@@ -0,0 +1,18 @@
+package jrw;
+
+import java.util.*;
+
+public abstract class Restart extends RuntimeException implements Handler {
+    public Restart() {}
+    public Restart(Throwable cause) {super(cause);}
+    public Restart(String msg) {super(msg);}
+    public Restart(String msg, Throwable cause) {super(msg, cause);}
+
+    public static Restart with(Handler handler) {
+       return(new Restart() {
+               public Map<Object, Object> handle(Request req) {
+                   return(handler.handle(req));
+               }
+           });
+    }
+}