changeset 1402:27efb1fcbcb5

move luan.lib to goodjava
author Franklin Schmidt <fschmidt@gmail.com>
date Tue, 17 Sep 2019 01:35:01 -0400
parents ef1620aa99cb
children 5e1075226662
files src/goodjava/json/JsonParser.java src/goodjava/json/JsonToString.java src/goodjava/logging/Log4jFactory.java src/goodjava/logging/Logger.java src/goodjava/logging/LoggerFactory.java src/goodjava/parser/ParseException.java src/goodjava/parser/Parser.java src/goodjava/queryparser/FieldParser.java src/goodjava/queryparser/MultiFieldParser.java src/goodjava/queryparser/NumberFieldParser.java src/goodjava/queryparser/SaneQueryParser.java src/goodjava/queryparser/StringFieldParser.java src/goodjava/queryparser/SynonymParser.java src/goodjava/rpc/FixedLengthInputStream.java src/goodjava/rpc/Rpc.java src/goodjava/rpc/RpcCall.java src/goodjava/rpc/RpcClient.java src/goodjava/rpc/RpcCon.java src/goodjava/rpc/RpcError.java src/goodjava/rpc/RpcException.java src/goodjava/rpc/RpcResult.java src/goodjava/rpc/RpcServer.java src/goodjava/webserver/Connection.java src/goodjava/webserver/Handler.java src/goodjava/webserver/Request.java src/goodjava/webserver/RequestParser.java src/goodjava/webserver/Response.java src/goodjava/webserver/ResponseOutputStream.java src/goodjava/webserver/Server.java src/goodjava/webserver/Status.java src/goodjava/webserver/Util.java src/goodjava/webserver/examples/Cookies.java src/goodjava/webserver/examples/Example.java src/goodjava/webserver/examples/Headers.java src/goodjava/webserver/examples/Params.java src/goodjava/webserver/examples/post.html src/goodjava/webserver/examples/post_multipart.html src/goodjava/webserver/handlers/ContentTypeHandler.java src/goodjava/webserver/handlers/DirHandler.java src/goodjava/webserver/handlers/DomainHandler.java src/goodjava/webserver/handlers/FileHandler.java src/goodjava/webserver/handlers/IndexHandler.java src/goodjava/webserver/handlers/ListHandler.java src/goodjava/webserver/handlers/LogHandler.java src/goodjava/webserver/handlers/MapHandler.java src/goodjava/webserver/handlers/SafeHandler.java src/luan/Luan.java src/luan/host/WebHandler.java src/luan/host/run.luan src/luan/lib/json/JsonParser.java src/luan/lib/json/JsonToString.java src/luan/lib/logging/Log4jFactory.java src/luan/lib/logging/Logger.java src/luan/lib/logging/LoggerFactory.java src/luan/lib/parser/ParseException.java src/luan/lib/parser/Parser.java src/luan/lib/queryparser/FieldParser.java src/luan/lib/queryparser/MultiFieldParser.java src/luan/lib/queryparser/NumberFieldParser.java src/luan/lib/queryparser/SaneQueryParser.java src/luan/lib/queryparser/StringFieldParser.java src/luan/lib/queryparser/SynonymParser.java src/luan/lib/rpc/FixedLengthInputStream.java src/luan/lib/rpc/Rpc.java src/luan/lib/rpc/RpcCall.java src/luan/lib/rpc/RpcClient.java src/luan/lib/rpc/RpcCon.java src/luan/lib/rpc/RpcError.java src/luan/lib/rpc/RpcException.java src/luan/lib/rpc/RpcResult.java src/luan/lib/rpc/RpcServer.java src/luan/lib/webserver/Connection.java src/luan/lib/webserver/Handler.java src/luan/lib/webserver/Request.java src/luan/lib/webserver/RequestParser.java src/luan/lib/webserver/Response.java src/luan/lib/webserver/ResponseOutputStream.java src/luan/lib/webserver/Server.java src/luan/lib/webserver/Status.java src/luan/lib/webserver/Util.java src/luan/lib/webserver/examples/Cookies.java src/luan/lib/webserver/examples/Example.java src/luan/lib/webserver/examples/Headers.java src/luan/lib/webserver/examples/Params.java src/luan/lib/webserver/examples/post.html src/luan/lib/webserver/examples/post_multipart.html src/luan/lib/webserver/handlers/ContentTypeHandler.java src/luan/lib/webserver/handlers/DirHandler.java src/luan/lib/webserver/handlers/DomainHandler.java src/luan/lib/webserver/handlers/FileHandler.java src/luan/lib/webserver/handlers/IndexHandler.java src/luan/lib/webserver/handlers/ListHandler.java src/luan/lib/webserver/handlers/LogHandler.java src/luan/lib/webserver/handlers/MapHandler.java src/luan/lib/webserver/handlers/SafeHandler.java src/luan/modules/Html.luan src/luan/modules/Parsers.luan src/luan/modules/Rpc.luan src/luan/modules/ThreadLuan.java src/luan/modules/http/Http.luan src/luan/modules/http/LuanDomainHandler.java src/luan/modules/http/LuanHandler.java src/luan/modules/http/NotFound.java src/luan/modules/http/Server.luan src/luan/modules/logging/LuanLogger.java src/luan/modules/lucene/Lucene.luan src/luan/modules/lucene/LuceneIndex.java src/luan/modules/lucene/PostgresBackup.java src/luan/modules/parsers/BBCode.java src/luan/modules/parsers/Css.java src/luan/modules/parsers/Csv.java src/luan/modules/parsers/Html.java src/luan/modules/parsers/Theme.java src/luan/modules/sql/Database.java src/luan/modules/url/LuanUrl.java src/luan/modules/url/MultipartClient.java src/luan/modules/url/WwwAuthenticate.java
diffstat 117 files changed, 3128 insertions(+), 3128 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/json/JsonParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,216 @@
+package goodjava.json;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Collections;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
+
+
+public final class JsonParser {
+
+	public static Object parse(String text) throws ParseException {
+		return new JsonParser(text).parse();
+	}
+
+	private final Parser parser;
+
+	private JsonParser(String text) {
+		this.parser = new Parser(text);
+	}
+
+	private ParseException exception(String msg) {
+		return new ParseException(parser,msg);
+	}
+
+	private Object parse() throws ParseException {
+		spaces();
+		Object value = value();
+		spaces();
+		if( !parser.endOfInput() )
+			throw exception("unexpected text");
+		return value;
+	}
+
+	private Object value() throws ParseException {
+		if( parser.match("null") )
+			return null;
+		if( parser.match("true") )
+			return Boolean.TRUE;
+		if( parser.match("false") )
+			return Boolean.FALSE;
+		String s = string();
+		if( s != null )
+			return s;
+		Number n = number();
+		if( n != null )
+			return n;
+		List a = array();
+		if( a != null )
+			return a;
+		Map o = object();
+		if( o != null )
+			return o;
+		throw exception("invalid value");
+	}
+
+	private String string() throws ParseException {
+		parser.begin();
+		if( !parser.match('"') )
+			return parser.failure(null);
+		StringBuilder sb = new StringBuilder();
+		while( parser.anyChar() ) {
+			char c = parser.lastChar();
+			switch(c) {
+			case '"':
+				return parser.success(sb.toString());
+			case '\\':
+				if( parser.anyChar() ) {
+					c = parser.lastChar();
+					switch(c) {
+					case '"':
+					case '\'':  // not in spec
+					case '\\':
+					case '/':
+						sb.append(c);
+						continue;
+					case 'b':
+						sb.append('\b');
+						continue;
+					case 'f':
+						sb.append('\f');
+						continue;
+					case 'n':
+						sb.append('\n');
+						continue;
+					case 'r':
+						sb.append('\r');
+						continue;
+					case 't':
+						sb.append('\t');
+						continue;
+					case 'u':
+						int n = 0;
+						for( int i=0; i<4; i++ ) {
+							int d;
+							if( parser.inCharRange('0','9') ) {
+								d = parser.lastChar() - '0';
+							} else if( parser.inCharRange('a','f') ) {
+								d = parser.lastChar() - 'a' + 10;
+							} else if( parser.inCharRange('A','F') ) {
+								d = parser.lastChar() - 'A' + 10;
+							} else {
+								throw exception("invalid hex digit");
+							}
+							n = 16*n + d;
+						}
+						sb.append((char)n);
+						continue;
+					}
+				}
+				throw exception("invalid escape char");
+			default:
+				sb.append(c);
+			}
+		}
+		parser.failure();
+		throw exception("unclosed string");
+	}
+
+	private Number number() {
+		int start = parser.begin();
+		boolean isFloat = false;
+		parser.match('-');
+		if( !parser.match('0') ) {
+			if( !parser.inCharRange('1','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+		}
+		if( parser.match('.') ) {
+			if( !parser.inCharRange('0','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+			isFloat = true;
+		}
+		if( parser.anyOf("eE") ) {
+			parser.anyOf("+-");
+			if( !parser.inCharRange('0','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+			isFloat = true;
+		}
+		String s = parser.textFrom(start);
+		Number n;
+		if(isFloat)
+			n = Double.valueOf(s);
+		else
+			n = Long.valueOf(s);
+		return parser.success(n);
+	}
+
+	private List array() throws ParseException {
+		parser.begin();
+		if( !parser.match('[') )
+			return parser.failure(null);
+		spaces();
+		if( parser.match(']') )
+			return parser.success(Collections.emptyList());
+		List list = new ArrayList();
+		list.add( value() );
+		spaces();
+		while( parser.match(',') ) {
+			spaces();
+			list.add( value() );
+			spaces();
+		}
+		if( parser.match(']') )
+			return parser.success(list);
+		if( parser.endOfInput() ) {
+			parser.failure();
+			throw exception("unclosed array");
+		}
+		throw exception("unexpected text in array");
+	}
+
+	private Map object() throws ParseException {
+		parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		spaces();
+		if( parser.match('}') )
+			return parser.success(Collections.emptyMap());
+		Map map = new LinkedHashMap();
+		addEntry(map);
+		while( parser.match(',') ) {
+			spaces();
+			addEntry(map);
+		}
+		if( parser.match('}') )
+			return parser.success(map);
+		if( parser.endOfInput() ) {
+			parser.failure();
+			throw exception("unclosed object");
+		}
+		throw exception("unexpected text in object");
+	}
+
+	private void addEntry(Map map) throws ParseException {
+		String key = string();
+		if( key==null )
+			throw exception("invalid object key");
+		spaces();
+		if( !parser.match(':') )
+			throw exception("':' expected");
+		spaces();
+		Object value = value();
+		spaces();
+		map.put(key,value);
+	}
+
+	private void spaces() {
+		while( parser.anyOf(" \t\r\n") );
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/json/JsonToString.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,169 @@
+package goodjava.json;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Iterator;
+
+
+public class JsonToString {
+
+	public static final class JsonException extends RuntimeException {
+		private JsonException(String msg) {
+			super(msg);
+		}
+	}
+
+	public static String toString(Object obj) throws JsonException {
+		StringBuilder sb = new StringBuilder();
+		new JsonToString().toString(obj,sb,0);
+		sb.append('\n');
+		return sb.toString();
+	}
+
+	public static String toCompressedString(Object obj) throws JsonException {
+		StringBuilder sb = new StringBuilder();
+		JsonToString jts = new JsonToString() {
+			void indent(StringBuilder sb,int indented) {}
+		};
+		jts.toString(obj,sb,0);
+		return sb.toString();
+	}
+
+	private void toString(Object obj,StringBuilder sb,int indented) throws JsonException {
+		if( obj == null || obj instanceof Boolean || obj instanceof Number ) {
+			sb.append(obj);
+			return;
+		}
+		if( obj instanceof String ) {
+			toString((String)obj,sb);
+			return;
+		}
+		if( obj instanceof List ) {
+			toString((List)obj,sb,indented);
+			return;
+		}
+		if( obj instanceof Map ) {
+			toString((Map)obj,sb,indented);
+			return;
+		}
+		throw new JsonException("can't handle type "+obj.getClass().getName());
+	}
+
+	private static void toString(final String s,StringBuilder sb) {
+		sb.append('"');
+		for( int i=0; i<s.length(); i++ ) {
+			char c = s.charAt(i);
+			switch(c) {
+			case '"':
+				sb.append("\\\"");
+				break;
+			case '\\':
+				sb.append("\\\\");
+				break;
+			case '\b':
+				sb.append("\\b");
+				break;
+			case '\f':
+				sb.append("\\f");
+				break;
+			case '\n':
+				sb.append("\\n");
+				break;
+			case '\r':
+				sb.append("\\r");
+				break;
+			case '\t':
+				sb.append("\\t");
+				break;
+			default:
+				sb.append(c);
+			}
+		}
+		sb.append('"');
+	}
+
+	public static String javascriptEncode(String s) {
+		StringBuilder sb = new StringBuilder();
+		for( int i=0; i<s.length(); i++ ) {
+			char c = s.charAt(i);
+			switch(c) {
+			case '"':
+				sb.append("\\\"");
+				break;
+			case '\'':  // added for javascript
+				sb.append("\\'");
+				break;
+			case '\\':
+				sb.append("\\\\");
+				break;
+			case '\b':
+				sb.append("\\b");
+				break;
+			case '\f':
+				sb.append("\\f");
+				break;
+			case '\n':
+				sb.append("\\n");
+				break;
+			case '\r':
+				sb.append("\\r");
+				break;
+			case '\t':
+				sb.append("\\t");
+				break;
+			default:
+				sb.append(c);
+			}
+		}
+		return sb.toString();
+	}
+
+	private void toString(List list,StringBuilder sb,int indented) {
+		sb.append('[');
+		if( !list.isEmpty() ) {
+			indent(sb,indented+1);
+			toString(list.get(0),sb,indented+1);
+			for( int i=1; i<list.size(); i++ ) {
+				sb.append(", ");
+				toString(list.get(i),sb,indented+1);
+			}
+			indent(sb,indented);
+		}
+		sb.append(']');
+		return;
+	}
+
+	private void toString(Map map,StringBuilder sb,int indented) throws JsonException {
+		sb.append('{');
+		if( !map.isEmpty() ) {
+			Iterator<Map.Entry> i = map.entrySet().iterator();
+			indent(sb,indented+1);
+			toString(i.next(),sb,indented+1);
+			while( i.hasNext() ) {
+				sb.append(',');
+				indent(sb,indented+1);
+				toString(i.next(),sb,indented+1);
+			}
+			indent(sb,indented);
+		}
+		sb.append('}');
+	}
+
+	private void toString(Map.Entry entry,StringBuilder sb,int indented) throws JsonException {
+		Object key = entry.getKey();
+		if( !(key instanceof String) )
+			throw new JsonException("table keys must be strings");
+		toString((String)key,sb);
+		sb.append(": ");
+		toString(entry.getValue(),sb,indented);
+	}
+
+	void indent(StringBuilder sb,int indented) {
+		sb.append('\n');
+		for( int i=0; i<indented; i++ ) {
+			sb.append('\t');
+		}
+	}
+
+	private JsonToString() {}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/logging/Log4jFactory.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,52 @@
+package goodjava.logging;
+
+
+public final class Log4jFactory extends LoggerFactory {
+	private static final class Log4jLogger implements Logger {
+		final org.apache.log4j.Logger log4j;
+
+		Log4jLogger(org.apache.log4j.Logger log4j) {
+			this.log4j = log4j;
+		}
+
+		@Override public void error(String msg) {
+			log4j.error(msg);
+		}
+
+		@Override public void error(String msg,Throwable t) {
+			log4j.error(msg,t);
+		}
+
+		@Override public void warn(String msg) {
+			log4j.warn(msg);
+		}
+
+		@Override public void warn(String msg,Throwable t) {
+			log4j.warn(msg,t);
+		}
+
+		@Override public void info(String msg) {
+			log4j.info(msg);
+		}
+
+		@Override public void info(String msg,Throwable t) {
+			log4j.info(msg,t);
+		}
+
+		@Override public void debug(String msg) {
+			log4j.debug(msg);
+		}
+
+		@Override public void debug(String msg,Throwable t) {
+			log4j.debug(msg,t);
+		}
+	}
+
+	@Override protected Logger getLoggerImpl(Class cls) {
+		return new Log4jLogger(org.apache.log4j.Logger.getLogger(cls));
+	}
+
+	@Override protected Logger getLoggerImpl(String name) {
+		return new Log4jLogger(org.apache.log4j.Logger.getLogger(name));
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/logging/Logger.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,14 @@
+package goodjava.logging;
+
+// Because slf4j is an overcomplicated mess that caches loggers when it shouldn't.
+
+public interface Logger {
+	public void error(String msg);
+	public void error(String msg,Throwable t);
+	public void warn(String msg);
+	public void warn(String msg,Throwable t);
+	public void info(String msg);
+	public void info(String msg,Throwable t);
+	public void debug(String msg);
+	public void debug(String msg,Throwable t);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/logging/LoggerFactory.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,17 @@
+package goodjava.logging;
+
+
+public abstract class LoggerFactory {
+	public static LoggerFactory implementation = new Log4jFactory();
+
+	protected abstract Logger getLoggerImpl(Class cls);
+	protected abstract Logger getLoggerImpl(String name);
+
+	public static Logger getLogger(Class cls) {
+		return implementation.getLoggerImpl(cls);
+	}
+
+	public static Logger getLogger(String name) {
+		return implementation.getLoggerImpl(name);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/parser/ParseException.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,71 @@
+package goodjava.parser;
+
+
+public final class ParseException extends Exception {
+	public final String text;
+	public final int errorIndex;
+	public final int highIndex;
+
+	public ParseException(Parser parser,String msg) {
+		super(msg);
+		this.text = parser.text;
+		this.errorIndex = parser.currentIndex();
+		this.highIndex = parser.highIndex();
+	}
+
+	public ParseException(Parser parser,Exception cause) {
+		this(parser,cause.getMessage(),cause);
+	}
+
+	public ParseException(Parser parser,String msg,Exception cause) {
+		super(msg,cause);
+		this.text = parser.text;
+		this.errorIndex = parser.currentIndex();
+		this.highIndex = parser.highIndex();
+	}
+
+	private class Location {
+		final int line;
+		final int pos;
+
+		Location(int index) {
+			int line = 0;
+			int i = -1;
+			while(true) {
+				int j = text.indexOf('\n',i+1);
+				if( j == -1 || j >= index )
+					break;
+				i = j;
+				line++;
+			}
+			this.line = line;
+			this.pos = index - i - 1;
+		}
+	}
+
+	private String[] lines() {
+		return text.split("\n",-1);
+	}
+
+	@Override public String getMessage() {
+		String line;
+		int pos;
+		StringBuilder sb = new StringBuilder(super.getMessage());
+		if( text.indexOf('\n') == -1 ) {
+			line = text;
+			pos = errorIndex;
+			sb.append( " (position " + (pos+1) + ")\n" );
+		} else {
+			Location loc = new Location(errorIndex);
+			line = lines()[loc.line];
+			pos = loc.pos;
+			sb.append( " (line " + (loc.line+1) + ", pos " + (pos+1) + ")\n" );
+		}
+		sb.append( line + "\n" );
+		for( int i=0; i<pos; i++ ) {
+			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
+		}
+		sb.append("^\n");
+		return sb.toString();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/parser/Parser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,156 @@
+package goodjava.parser;
+
+
+public class Parser {
+	public final String text;
+	private final int len;
+	private int[] stack = new int[256];
+	private int frame = 0;
+	private int iHigh;
+
+	public Parser(String text) {
+		this.text = text;
+		this.len = text.length();
+	}
+
+	private int i() {
+		return stack[frame];
+	}
+
+	private void i(int i) {
+		stack[frame] += i;
+		if( iHigh < stack[frame] )
+			iHigh = stack[frame];
+	}
+
+	public int begin() {
+		frame++;
+		if( frame == stack.length ) {
+			int[] a = new int[2*frame];
+			System.arraycopy(stack,0,a,0,frame);
+			stack = a;
+		}
+		stack[frame] = stack[frame-1];
+		return i();
+	}
+
+	public void rollback() {
+		stack[frame] = frame==0 ? 0 : stack[frame-1];
+	}
+
+	public <T> T success(T t) {
+		success();
+		return t;
+	}
+
+	public boolean success() {
+		frame--;
+		stack[frame] = stack[frame+1];
+		return true;
+	}
+
+	public <T> T failure(T t) {
+		failure();
+		return t;
+	}
+
+	public boolean failure() {
+		frame--;
+		return false;
+	}
+
+	public int currentIndex() {
+		return i();
+	}
+/*
+	public int errorIndex() {
+		return frame > 0 ? stack[frame-1] : 0;
+	}
+*/
+	public int highIndex() {
+		return iHigh;
+	}
+
+	public char lastChar() {
+		return text.charAt(i()-1);
+	}
+
+	public char currentChar() {
+		return text.charAt(i());
+	}
+
+	public boolean endOfInput() {
+		return i() >= len;
+	}
+
+	public boolean match(char c) {
+		if( endOfInput() || text.charAt(i()) != c )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean match(String s) {
+		int n = s.length();
+		if( !text.regionMatches(i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean matchIgnoreCase(String s) {
+		int n = s.length();
+		if( !text.regionMatches(true,i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean anyOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean noneOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean inCharRange(char cLow, char cHigh) {
+		if( endOfInput() )
+			return false;
+		char c = text.charAt(i());
+		if( !(cLow <= c && c <= cHigh) )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean anyChar() {
+		if( endOfInput() )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean test(char c) {
+		return !endOfInput() && text.charAt(i()) == c;
+	}
+
+	public boolean test(String s) {
+		return text.regionMatches(i(),s,0,s.length());
+	}
+
+	public boolean testIgnoreCase(String s) {
+		return text.regionMatches(true,i(),s,0,s.length());
+	}
+
+	public String textFrom(int start) {
+		return text.substring(start,i());
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/FieldParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,12 @@
+package goodjava.queryparser;
+
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+import goodjava.parser.ParseException;
+
+
+public interface FieldParser {
+	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException;
+	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException;
+	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/MultiFieldParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,86 @@
+package goodjava.queryparser;
+
+import java.util.Map;
+import java.util.HashMap;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.SortField;
+import goodjava.parser.ParseException;
+
+
+public class MultiFieldParser implements FieldParser {
+
+	/**
+	 * maps field name to FieldParser
+	 */
+	public final Map<String,FieldParser> fields = new HashMap<String,FieldParser>();
+	public boolean allowUnspecifiedFields = false;
+	private final FieldParser defaultFieldParser;
+	private final String[] defaultFields;
+
+	public MultiFieldParser() {
+		this.defaultFieldParser = null;
+		this.defaultFields = null;
+	}
+
+	public MultiFieldParser(FieldParser defaultFieldParser,String... defaultFields) {
+		this.defaultFieldParser = defaultFieldParser;
+		this.defaultFields = defaultFields;
+		for( String field : defaultFields ) {
+			fields.put(field,defaultFieldParser);
+		}
+	}
+
+	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		if( field == null ) {
+			if( defaultFieldParser == null )
+				throw qp.exception("no defaults were specified, so a field is required");
+			if( defaultFields.length == 1 )
+				return defaultFieldParser.getQuery(qp,defaultFields[0],query);
+			BooleanQuery bq = new BooleanQuery();
+			for( String f : defaultFields ) {
+				bq.add( defaultFieldParser.getQuery(qp,f,query), BooleanClause.Occur.SHOULD );
+			}
+			return bq;
+		} else {
+			FieldParser fp = fields.get(field);
+			if( fp != null )
+				return fp.getQuery(qp,field,query);
+			if( allowUnspecifiedFields )
+				return defaultFieldParser.getQuery(qp,field,query);
+			throw qp.exception("unrecognized field '"+field+"'");
+		}
+	}
+
+	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		if( field == null ) {
+			if( defaultFieldParser == null )
+				throw qp.exception("no defaults were specified, so a field is required");
+			if( defaultFields.length == 1 )
+				return defaultFieldParser.getRangeQuery(qp,defaultFields[0],minQuery,maxQuery,includeMin,includeMax);
+			BooleanQuery bq = new BooleanQuery();
+			for( String f : defaultFields ) {
+				bq.add( defaultFieldParser.getRangeQuery(qp,f,minQuery,maxQuery,includeMin,includeMax), BooleanClause.Occur.SHOULD );
+			}
+			return bq;
+		} else {
+			FieldParser fp = fields.get(field);
+			if( fp != null )
+				return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+			if( allowUnspecifiedFields )
+				return defaultFieldParser.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+			throw qp.exception("field '"+field+"' not specified");
+		}
+	}
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
+		FieldParser fp = fields.get(field);
+		if( fp != null )
+			return fp.getSortField(qp,field,reverse);
+		if( allowUnspecifiedFields )
+			return defaultFieldParser.getSortField(qp,field,reverse);
+		throw qp.exception("field '"+field+"' not specified");
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/NumberFieldParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,84 @@
+package goodjava.queryparser;
+
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.SortField;
+import goodjava.parser.ParseException;
+
+
+public abstract class NumberFieldParser implements FieldParser {
+
+	@Override public final Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		return getRangeQuery(qp,field,query,query,true,true);
+	}
+
+	@Override public final Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		try {
+			return getRangeQuery(field,minQuery,maxQuery,includeMin,includeMax);
+		} catch(NumberFormatException e) {
+			throw qp.exception(e);
+		}
+	}
+
+	abstract protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax);
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
+		return new SortField( field, sortType(), reverse );
+	}
+
+	abstract protected SortField.Type sortType();
+
+
+	public static final FieldParser INT = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			int min = Integer.parseInt(minQuery);
+			int max = Integer.parseInt(maxQuery);
+			return NumericRangeQuery.newIntRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.INT;
+		}
+	};
+
+	public static final FieldParser LONG = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			long min = Long.parseLong(minQuery);
+			long max = Long.parseLong(maxQuery);
+			return NumericRangeQuery.newLongRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.LONG;
+		}
+	};
+
+	public static final FieldParser FLOAT = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			float min = Float.parseFloat(minQuery);
+			float max = Float.parseFloat(maxQuery);
+			return NumericRangeQuery.newFloatRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.FLOAT;
+		}
+	};
+
+	public static final FieldParser DOUBLE = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			double min = Double.parseDouble(minQuery);
+			double max = Double.parseDouble(maxQuery);
+			return NumericRangeQuery.newDoubleRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.DOUBLE;
+		}
+	};
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/SaneQueryParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,261 @@
+package goodjava.queryparser;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
+
+
+public class SaneQueryParser {
+
+	public static Query parseQuery(FieldParser fieldParser,String query) throws ParseException {
+		return new SaneQueryParser(fieldParser,query).parseQuery();
+	}
+
+	private static Pattern specialChar = Pattern.compile("[ \\t\\r\\n\":\\[\\]{}^+\\-(),?*\\\\]");
+
+	public static String literal(String s) {
+		return specialChar.matcher(s).replaceAll("\\\\$0");
+	}
+
+	public static Sort parseSort(FieldParser fieldParser,String sort) throws ParseException {
+		return new SaneQueryParser(fieldParser,sort).parseSort();
+	}
+
+
+	private static final String NOT_IN_RANGE = " \t\r\n\":[]{}^+()";
+	private static final String NOT_IN_TERM = NOT_IN_RANGE + "-";
+	private static final String NOT_IN_FIELD = NOT_IN_TERM + ",";
+	private final FieldParser fieldParser;
+	private final Parser parser;
+
+	private SaneQueryParser(FieldParser fieldParser,String query) {
+		this.fieldParser = fieldParser;
+		this.parser = new Parser(query);
+		parser.begin();
+	}
+
+	ParseException exception(String msg) {
+		parser.failure();
+		return new ParseException(parser,msg);
+	}
+
+	ParseException exception(Exception cause) {
+		parser.failure();
+		return new ParseException(parser,cause);
+	}
+
+	private Query parseQuery() throws ParseException {
+		Spaces();
+		BooleanQuery bq = new BooleanQuery();
+		while( !parser.endOfInput() ) {
+			bq.add( Term(null) );
+		}
+		BooleanClause[] clauses = bq.getClauses();
+		switch( clauses.length ) {
+		case 0:
+			return new MatchAllDocsQuery();
+		case 1:
+			{
+				BooleanClause bc = clauses[0];
+				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
+					return bc.getQuery();
+			}
+		default:
+			return bq;
+		}
+	}
+
+	private BooleanClause Term(String defaultField) throws ParseException {
+		BooleanClause.Occur occur;
+		if( parser.match('+') ) {
+			occur = BooleanClause.Occur.MUST;
+			Spaces();
+		} else if( parser.match('-') ) {
+			occur = BooleanClause.Occur.MUST_NOT;
+			Spaces();
+		} else {
+			occur = BooleanClause.Occur.SHOULD;
+		}
+		String field = QueryField();
+		if( field == null )
+			field = defaultField;
+		Query query = NestedTerm(field);
+		if( query == null )
+			query = RangeTerm(field);
+		if( query == null ) {
+			parser.begin();
+			String match = SimpleTerm(NOT_IN_TERM);
+			query = fieldParser.getQuery(this,field,match);
+			parser.success();
+		}
+		if( parser.match('^') ) {
+			Spaces();
+			int start = parser.begin();
+			try {
+				while( parser.anyOf("0123456789.") );
+				String match = parser.textFrom(start);
+				float boost = Float.parseFloat(match);
+				query.setBoost(boost);
+			} catch(NumberFormatException e) {
+				throw exception(e);
+			}
+			parser.success();
+			Spaces();
+		}
+		BooleanClause bc = new BooleanClause(query,occur);
+		return bc;
+	}
+
+	private Query NestedTerm(String field) throws ParseException {
+		parser.begin();
+		if( !parser.match('(') )
+			return parser.failure(null);
+		BooleanQuery bq = new BooleanQuery();
+		while( !parser.match(')') ) {
+			if( parser.endOfInput() )
+				throw exception("unclosed parentheses");
+			bq.add( Term(field) );
+		}
+		Spaces();
+		BooleanClause[] clauses = bq.getClauses();
+		switch( clauses.length ) {
+		case 0:
+			throw exception("empty parentheses");
+		case 1:
+			{
+				BooleanClause bc = clauses[0];
+				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
+					return parser.success(bc.getQuery());
+			}
+		default:
+			return parser.success(bq);
+		}
+	}
+
+	private Query RangeTerm(String field) throws ParseException {
+		parser.begin();
+		if( !parser.anyOf("[{") )
+			return parser.failure(null);
+		boolean includeMin = parser.lastChar() == '[';
+		Spaces();
+		String minQuery = SimpleTerm(NOT_IN_RANGE);
+		TO();
+		String maxQuery = SimpleTerm(NOT_IN_RANGE);
+		if( !parser.anyOf("]}") )
+			throw exception("unclosed range");
+		boolean includeMax = parser.lastChar() == ']';
+		Spaces();
+		Query query = fieldParser.getRangeQuery(this,field,minQuery,maxQuery,includeMin,includeMax);
+		return parser.success(query);
+	}
+
+	private void TO() throws ParseException {
+		parser.begin();
+		if( !(parser.match("TO") && Space()) )
+			throw exception("'TO' expected");
+		Spaces();
+		parser.success();
+	}
+
+	private String SimpleTerm(String exclude) throws ParseException {
+		parser.begin();
+		String match;
+		if( parser.match('"') ) {
+			int start = parser.currentIndex() - 1;
+			while( !parser.match('"') ) {
+				if( parser.endOfInput() )
+					throw exception("unclosed quotes");
+				parser.anyChar();
+				checkEscape();
+			}
+			match = parser.textFrom(start);
+			Spaces();
+		} else {
+			match = Unquoted(exclude);
+		}
+		if( match.length() == 0 )
+			throw exception("invalid input");
+		return parser.success(match);
+	}
+
+	private String QueryField() throws ParseException {
+		parser.begin();
+		String match = Field();
+		if( match==null || !parser.match(':') )
+			return parser.failure((String)null);
+		Spaces();
+		return parser.success(match);
+	}
+
+	private String Field() throws ParseException {
+		parser.begin();
+		String match = Unquoted(NOT_IN_FIELD);
+		if( match.length()==0 )
+			return parser.failure((String)null);
+		match = StringFieldParser.escape(this,match);
+		return parser.success(match);
+	}
+
+	private String Unquoted(String exclude) throws ParseException {
+		int start = parser.begin();
+		while( parser.noneOf(exclude) ) {
+			checkEscape();
+		}
+		String match = parser.textFrom(start);
+		Spaces();
+		return parser.success(match);
+	}
+
+	private void checkEscape() {
+		if( parser.lastChar() == '\\' )
+			parser.anyChar();
+	}
+
+	private void Spaces() {
+		while( Space() );
+	}
+
+	private boolean Space() {
+		return parser.anyOf(" \t\r\n");
+	}
+
+
+	// sort
+
+	private Sort parseSort() throws ParseException {
+		Spaces();
+		if( parser.endOfInput() )
+			return null;
+		List<SortField> list = new ArrayList<SortField>();
+		list.add( SortField() );
+		while( !parser.endOfInput() ) {
+			parser.begin();
+			if( !parser.match(',') )
+				throw exception("',' expected");
+			Spaces();
+			parser.success();
+			list.add( SortField() );
+		}
+		return new Sort(list.toArray(new SortField[0]));
+	}
+
+	private SortField SortField() throws ParseException {
+		parser.begin();
+		String field = Field();
+		if( field==null )
+			throw exception("invalid input");
+		boolean reverse = !parser.matchIgnoreCase("asc") && parser.matchIgnoreCase("desc");
+		Spaces();
+		SortField sf = fieldParser.getSortField(this,field,reverse);
+		return parser.success(sf);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/StringFieldParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,113 @@
+package goodjava.queryparser;
+
+import java.io.StringReader;
+import java.io.IOException;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TermRangeQuery;
+import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.index.Term;
+import goodjava.parser.ParseException;
+
+
+public class StringFieldParser implements FieldParser {
+	public int slop = 0;
+	public final Analyzer analyzer;
+
+	public StringFieldParser(Analyzer analyzer) {
+		this.analyzer = analyzer;
+	}
+
+	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		String wildcard = wildcard(qp,query);
+		if( wildcard != null )
+			return new WildcardQuery(new Term(field,wildcard));
+		if( query.endsWith("*") && !query.endsWith("\\*") )
+			return new PrefixQuery(new Term(field,query.substring(0,query.length()-1)));
+		query = escape(qp,query);
+		PhraseQuery pq = new PhraseQuery();
+		try {
+			TokenStream ts = analyzer.tokenStream(field,new StringReader(query));
+			CharTermAttribute termAttr = ts.addAttribute(CharTermAttribute.class);
+			PositionIncrementAttribute posAttr = ts.addAttribute(PositionIncrementAttribute.class);
+			ts.reset();
+			int pos = -1;
+			while( ts.incrementToken() ) {
+				pos += posAttr.getPositionIncrement();
+				pq.add( new Term(field,termAttr.toString()), pos );
+			}
+			ts.end();
+			ts.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		Term[] terms = pq.getTerms();
+		if( terms.length==1 && pq.getPositions()[0]==0 )
+			return new TermQuery(terms[0]);
+		return pq;
+	}
+
+	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		minQuery = escape(qp,minQuery);
+		maxQuery = escape(qp,maxQuery);
+		return TermRangeQuery.newStringRange(field,minQuery,maxQuery,includeMin,includeMax);
+	}
+
+	static String escape(SaneQueryParser qp,String s) throws ParseException {
+		final char[] a = s.toCharArray();
+		int i, n;
+		if( a[0] == '"' ) {
+			if( a[a.length-1] != '"' )  throw new RuntimeException();
+			i = 1;
+			n = a.length - 1;
+		} else {
+			i = 0;
+			n = a.length;
+		}
+		StringBuilder sb = new StringBuilder();
+		for( ; i<n; i++ ) {
+			char c = a[i];
+			if( c == '\\' ) {
+				if( ++i == a.length )
+					throw qp.exception("ends with '\\'");
+				c = a[i];
+			}
+			sb.append(c);
+		}
+		return sb.toString();
+	}
+
+	private static String wildcard(SaneQueryParser qp,String s) throws ParseException {
+		final char[] a = s.toCharArray();
+		if( a[0] == '"' )
+			return null;
+		boolean hasWildcard = false;
+		StringBuilder sb = new StringBuilder();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			if( c=='?' || c=='*' && i<a.length-1 )
+				hasWildcard = true;
+			if( c == '\\' ) {
+				if( ++i == a.length )
+					throw qp.exception("ends with '\\'");
+				c = a[i];
+				if( c=='?' || c=='*' )
+					sb.append('\\');
+			}
+			sb.append(c);
+		}
+		return hasWildcard ? sb.toString() : null;
+	}
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
+		return new SortField( field, SortField.Type.STRING, reverse );
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/queryparser/SynonymParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,43 @@
+package goodjava.queryparser;
+
+import java.util.Map;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.SortField;
+import goodjava.parser.ParseException;
+
+
+public class SynonymParser implements FieldParser {
+	private final FieldParser fp;
+	private final Map<String,String[]> synonymMap;
+
+	public SynonymParser(FieldParser fp,Map<String,String[]> synonymMap) {
+		this.fp = fp;
+		this.synonymMap = synonymMap;
+	}
+
+	protected String[] getSynonyms(String query) {
+		return synonymMap.get(query);
+	}
+
+	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		String[] synonyms = getSynonyms(query);
+		if( synonyms == null )
+			return fp.getQuery(qp,field,query);
+		BooleanQuery bq = new BooleanQuery();
+		bq.add( fp.getQuery(qp,field,query), BooleanClause.Occur.SHOULD );
+		for( String s : synonyms ) {
+			bq.add( fp.getQuery(qp,field,s), BooleanClause.Occur.SHOULD );
+		}
+		return bq;
+	}
+
+	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+	}
+
+	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
+		return fp.getSortField(qp,field,reverse);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/FixedLengthInputStream.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,75 @@
+package goodjava.rpc;
+
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.EOFException;
+
+
+public class FixedLengthInputStream extends FilterInputStream {
+	private long left;
+
+	public FixedLengthInputStream(InputStream in,long len) {
+		super(in);
+		if( len < 0 )
+			throw new IllegalArgumentException("len can't be negative");
+		this.left = len;
+	}
+
+	public int read() throws IOException {
+		if( left == 0 )
+			return -1;
+		int n = in.read();
+		if( n == -1 )
+			throw new EOFException();
+		left--;
+		return n;
+	}
+
+	public int read(byte b[], int off, int len) throws IOException {
+		if( len == 0 )
+			return 0;
+		if( left == 0 )
+			return -1;
+		if( len > left )
+			len = (int)left;
+		int n = in.read(b, off, len);
+		if( n == -1 )
+			throw new EOFException();
+		left -= n;
+		return n;
+	}
+
+	public long skip(long n) throws IOException {
+		if( n > left )
+			n = left;
+		n = in.skip(n);
+		left -= n;
+		return n;
+	}
+
+	public int available() throws IOException {
+		int n = in.available();
+		if( n > left )
+			n = (int)left;
+		return n;
+	}
+
+    public void close() throws IOException {
+        while( left > 0 ) {
+			if( skip(left) == 0 )
+				throw new EOFException();
+		}
+    }
+
+	public void mark(int readlimit) {}
+
+	public void reset() throws IOException {
+		throw new IOException("not supported");
+	}
+
+	public boolean markSupported() {
+		return false;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/Rpc.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,36 @@
+package goodjava.rpc;
+
+import java.io.IOException;
+
+
+// static utils
+public class Rpc {
+	private Rpc() {}  // never
+
+	public static final RpcResult OK = new RpcResult();
+
+	public static final RpcCall CLOSE = new RpcCall("close");
+	public static final RpcCall PING = new RpcCall("ping");
+	public static final String ECHO = "echo";
+
+	public static final RpcException COMMAND_NOT_FOUND = new RpcException("command_not_found");
+
+	public static boolean handle(RpcServer server,RpcCall call)
+		throws IOException
+	{
+		if( CLOSE.cmd.equals(call.cmd) ) {
+			server.close();
+			return true;
+		}
+		if( PING.cmd.equals(call.cmd) ) {
+			server.write(OK);
+			return true;
+		}
+		if( ECHO.equals(call.cmd) ) {
+			server.write(new RpcResult(call.args));
+			return true;
+		}
+		return false;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcCall.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,22 @@
+package goodjava.rpc;
+
+import java.io.InputStream;
+
+
+public final class RpcCall {
+	public final InputStream in;
+	public final long lenIn;
+	public final String cmd;
+	public final Object[] args;
+
+	public RpcCall(String cmd,Object... args) {
+		this(null,-1L,cmd,args);
+	}
+
+	public RpcCall(InputStream in,long lenIn,String cmd,Object... args) {
+		this.in = in;
+		this.lenIn = lenIn;
+		this.cmd = cmd;
+		this.args = args;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcClient.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,41 @@
+package goodjava.rpc;
+
+import java.net.Socket;
+import java.util.List;
+import java.util.ArrayList;
+
+
+public class RpcClient extends RpcCon {
+
+	public RpcClient(Socket socket)
+		throws RpcError
+	{
+		super(socket);
+	}
+
+	public void write(RpcCall call)
+		throws RpcError
+	{
+		List list = new ArrayList();
+		list.add(call.cmd);
+		for( Object arg : call.args ) {
+			list.add(arg);
+		}
+		write(call.in,call.lenIn,list);
+	}
+
+	public RpcResult read()
+		throws RpcError, RpcException
+	{
+		List list = readJson();
+		boolean ok = (Boolean)list.remove(0);
+		if( !ok ) {
+			String errorId = (String)list.remove(0);
+			Object[] args = list.toArray();
+			throw new RpcException(inBinary,lenBinary,errorId,args);
+		}
+		Object[] args = list.toArray();
+		return new RpcResult(inBinary,lenBinary,args);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcCon.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,132 @@
+package goodjava.rpc;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.io.EOFException;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import goodjava.parser.ParseException;
+import goodjava.json.JsonParser;
+import goodjava.json.JsonToString;
+
+
+public class RpcCon {
+	final Socket socket;
+	final InputStream in;
+	final OutputStream out;
+	InputStream inBinary = null;
+	long lenBinary = -1;
+	boolean readSome = false;
+
+	RpcCon(Socket socket)
+		throws RpcError
+	{
+		try {
+			this.socket = socket;
+			this.in = socket.getInputStream();
+			this.out = socket.getOutputStream();
+		} catch(IOException e) {
+			close();
+			throw new RpcError(e);
+		}
+	}
+
+	public void close()
+		throws RpcError
+	{
+		try {
+			socket.close();
+		} catch(IOException e) {
+			throw new RpcError(e);
+		}
+	}
+
+	public boolean isClosed() {
+		return socket.isClosed();
+	}
+
+	void write(InputStream in,long lenIn,List list)
+		throws RpcError
+	{
+		if( in != null )
+			list.add(0,lenIn);
+		String json = JsonToString.toString(list);
+		byte[] aJson = json.getBytes(StandardCharsets.UTF_8);
+		int len = aJson.length;
+		byte[] a = new byte[4+len];
+        a[0] = (byte)(len >>> 24);
+        a[1] = (byte)(len >>> 16);
+        a[2] = (byte)(len >>>  8);
+        a[3] = (byte)(len >>>  0);
+		System.arraycopy(aJson,0,a,4,len);
+		try {
+			out.write(a);
+			if( in != null ) {
+				a = new byte[8192];
+				long total = 0;
+				int n;
+				while( (n=in.read(a)) != -1 ) {
+					out.write(a,0,n);
+					total += n;
+				}
+				if( total != lenIn ) {
+					close();
+					throw new RpcError("InputStream wrong length "+total+" when should be "+lenIn);
+				}
+			}
+		} catch(IOException e) {
+			close();
+			throw new RpcError(e);
+		}
+	}
+
+	List readJson()
+		throws RpcError
+	{
+		try {
+			if( inBinary != null ) {
+				inBinary.close();
+				inBinary = null;
+				lenBinary = -1;
+			}
+			readSome = false;
+			byte[] a = new byte[4];
+			readAll(a);
+			int len = 0;
+			for( byte b : a ) {
+				len <<= 8;
+				len |= b&0xFF;
+			}
+			a = new byte[len];
+			readAll(a);
+			String json = new String(a,StandardCharsets.UTF_8);
+			List list = (List)JsonParser.parse(json);
+			if( list.get(0) instanceof Long ) {
+				lenBinary = (Long)list.remove(0);
+				inBinary = new FixedLengthInputStream(in,lenBinary);
+			}
+			return list;
+		} catch(IOException e) {
+			close();
+			throw new RpcError(e);
+		} catch(ParseException e) {
+			close();
+			throw new RpcError(e);
+		}
+	}
+
+	private void readAll(final byte[] a) throws IOException {
+		int total = 0;
+		int n;
+		while( total < a.length ){
+			n = in.read( a, total, a.length-total );
+			if( n == -1 )
+				throw new EOFException();
+			readSome = true;
+			total += n;
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcError.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,14 @@
+package goodjava.rpc;
+
+
+public class RpcError extends RuntimeException {
+
+	public RpcError(String msg) {
+		super(msg);
+	}
+
+	public RpcError(Exception e) {
+		super(e);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcException.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,21 @@
+package goodjava.rpc;
+
+import java.io.InputStream;
+
+
+public class RpcException extends Exception {
+	public final InputStream in;
+	public final long lenIn;
+	public final Object[] values;
+
+	public RpcException(String id,Object... values) {
+		this(null,-1,id,values);
+	}
+
+	public RpcException(InputStream in,long lenIn,String id,Object... values) {
+		super(id);
+		this.in = in;
+		this.lenIn = lenIn;
+		this.values = values;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcResult.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,20 @@
+package goodjava.rpc;
+
+import java.io.InputStream;
+
+
+public final class RpcResult {
+	public final InputStream in;
+	public final long lenIn;
+	public final Object[] returnValues;
+
+	public RpcResult(Object... returnValues) {
+		this(null,-1L,returnValues);
+	}
+
+	public RpcResult(InputStream in,long lenIn,Object... returnValues) {
+		this.in = in;
+		this.lenIn = lenIn;
+		this.returnValues = returnValues;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/rpc/RpcServer.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,55 @@
+package goodjava.rpc;
+
+import java.io.EOFException;
+import java.net.Socket;
+import java.util.List;
+import java.util.ArrayList;
+
+
+public class RpcServer extends RpcCon {
+
+	public RpcServer(Socket socket)
+		throws RpcError
+	{
+		super(socket);
+	}
+
+	public RpcCall read()
+		throws RpcError
+	{
+		try {
+			List list = readJson();
+			String cmd = (String)list.remove(0);
+			Object[] args = list.toArray();
+			return new RpcCall(inBinary,lenBinary,cmd,args);
+		} catch(RpcError e) {
+			if( !readSome && e.getCause() instanceof EOFException )
+				return null;
+			throw e;
+		}
+	}
+
+	public void write(RpcResult result)
+		throws RpcError
+	{
+		List list = new ArrayList();
+		list.add(true);
+		for( Object val : result.returnValues ) {
+			list.add(val);
+		}
+		write(result.in,result.lenIn,list);
+	}
+
+	public void write(RpcException ex)
+		throws RpcError
+	{
+		List list = new ArrayList();
+		list.add(false);
+		list.add(ex.getMessage());
+		for( Object val : ex.values ) {
+			list.add(val);
+		}
+		write(ex.in,ex.lenIn,list);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Connection.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,134 @@
+package goodjava.webserver;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.parser.ParseException;
+
+
+final class Connection {
+	private static final Logger logger = LoggerFactory.getLogger(Connection.class);
+
+	static void handle(Server server,Socket socket) {
+		new Connection(server,socket).handle();
+	}
+
+	private final Server server;
+	private final Socket socket;
+
+	private Connection(Server server,Socket socket) {
+		this.server = server;
+		this.socket = socket;
+	}
+
+	private void handle() {
+		try {
+			Request request = new Request();
+			Response response;
+			try {
+				{
+					InputStream in = socket.getInputStream();
+					byte[] a = new byte[8192];
+					int endOfHeader;
+					int size = 0;
+					int left = a.length;
+					outer: while(true) {
+						int n = in.read(a,size,left);
+						if( n == -1 ) {
+							if( size == 0 ) {
+								socket.close();
+								return;
+							}
+							throw new IOException("unexpected end of input at "+size);
+						}
+						size += n;
+						for( int i=0; i<=size-4; i++ ) {
+							if( a[i]=='\r' && a[i+1]=='\n' && a[i+2]=='\r' && a[i+3]=='\n' ) {
+								endOfHeader = i + 4;
+								break outer;
+							}
+						}
+						left -= n;
+						if( left == 0 ) {
+							byte[] a2 = new byte[2*a.length];
+							System.arraycopy(a,0,a2,0,size);
+							a = a2;
+							left = a.length - size;
+						}
+					}
+					String rawHead = new String(a,0,endOfHeader);
+					//System.out.println(rawHead);
+					request.rawHead = rawHead;
+					RequestParser parser = new RequestParser(request);
+					parser.parseHead();
+		
+					String lenStr = (String)request.headers.get("content-length");
+					if( lenStr != null ) {
+						int len = Integer.parseInt(lenStr);
+						byte[] body = new byte[len];
+						size -= endOfHeader;
+						System.arraycopy(a,endOfHeader,body,0,size);
+						while( size < len ) {
+							int n = in.read(body,size,len-size);
+							if( n == -1 ) {
+								throw new IOException("unexpected end of input at "+size);
+							}
+							size += n;
+						}
+						request.body = body;
+						//System.out.println(new String(request.body));
+					}
+	
+					String contentType = (String)request.headers.get("content-type");
+					if( contentType != null ) {
+						contentType = contentType.toLowerCase();
+						if( "application/x-www-form-urlencoded".equals(contentType) ) {
+							parser.parseUrlencoded(null);
+						} else if( "application/x-www-form-urlencoded; charset=utf-8".equals(contentType) ) {
+							parser.parseUrlencoded("utf-8");
+						} else if( contentType.startsWith("multipart/form-data;") ) {
+							parser.parseMultipart();
+						} else if( contentType.equals("application/json; charset=utf-8") ) {
+							parser.parseJson();
+						} else {
+							logger.info("unknown request content-type: "+contentType);
+						}
+					}
+
+					String scheme = (String)request.headers.get("x-forwarded-proto");
+					if( scheme != null )
+						request.scheme = scheme;
+				}
+				response = server.handler.handle(request);
+			} catch(ParseException e) {
+				logger.warn("parse error\n"+request.rawHead.trim()+"\n",e);
+				response = Response.errorResponse(Status.BAD_REQUEST,e.toString());
+			}
+			response.headers.put("connection","close");
+			response.headers.put("content-length",Long.toString(response.body.length));
+			byte[] header = response.toHeaderString().getBytes();
+	
+			OutputStream out = socket.getOutputStream();
+			out.write(header);
+			copyAll(response.body.content,out);
+			out.close();
+			socket.close();
+		} catch(IOException e) {
+			logger.info("",e);
+		}
+	}
+
+	private static void copyAll(InputStream in,OutputStream out)
+		throws IOException
+	{
+		byte[] a = new byte[8192];
+		int n;
+		while( (n=in.read(a)) != -1 ) {
+			out.write(a,0,n);
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Handler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,6 @@
+package goodjava.webserver;
+
+
+public interface Handler {
+	public Response handle(Request request);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Request.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,36 @@
+package goodjava.webserver;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Collections;
+
+
+public class Request {
+	public volatile String rawHead;
+	public volatile String method;
+	public volatile String rawPath;
+	public volatile String originalPath;
+	public volatile String path;
+	public volatile String protocol;  // only HTTP/1.1 is accepted
+	public volatile String scheme;
+	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	public final Map<String,Object> parameters = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	public final Map<String,String> cookies = Collections.synchronizedMap(new LinkedHashMap<String,String>());
+	public volatile byte[] body;
+
+	public static final class MultipartFile {
+		public final String filename;
+		public final String contentType;
+		public final Object content;  // byte[] or String
+
+		public MultipartFile(String filename,String contentType,Object content) {
+			this.filename = filename;
+			this.contentType = contentType;
+			this.content = content;
+		}
+
+		public String toString() {
+			return "{filename="+filename+", content="+content+"}";
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/RequestParser.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,287 @@
+package goodjava.webserver;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.List;
+import java.util.ArrayList;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
+
+
+final class RequestParser {
+	private static final Logger logger = LoggerFactory.getLogger(RequestParser.class);
+	private final Request request;
+	private Parser parser;
+
+	RequestParser(Request request) {
+		this.request = request;
+	}
+
+	void parseUrlencoded(String charset) throws ParseException, UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		this.parser = new Parser(Util.toString(request.body,charset));
+		parseQuery();
+		require( parser.endOfInput() );
+	}
+
+	void parseHead() throws ParseException {
+		this.parser = new Parser(request.rawHead);
+		parseRequestLine();
+		while( !parser.match("\r\n") ) {
+			parserHeaderField();
+		}
+		parseCookies();
+	}
+
+	private void parseRequestLine() throws ParseException {
+		parseMethod();
+		require( parser.match(' ') );
+		parseRawPath();
+		require( parser.match(' ') );
+		parseProtocol();
+		require( parser.match("\r\n") );
+	}
+
+	private void parseMethod() throws ParseException {
+		int start = parser.currentIndex();
+		if( !methodChar() )
+			throw new ParseException(parser,"no method");
+		while( methodChar() );
+		request.method = parser.textFrom(start);
+	}
+
+	private boolean methodChar() {
+		return parser.inCharRange('A','Z');
+	}
+
+	private void parseRawPath() throws ParseException {
+		int start = parser.currentIndex();
+		parsePath();
+		if( parser.match('?') )
+			parseQuery();
+		request.rawPath = parser.textFrom(start);
+	}
+
+	private void parsePath() throws ParseException {
+		int start = parser.currentIndex();
+		if( !parser.match('/') )
+			throw new ParseException(parser,"bad path");
+		while( parser.noneOf(" ?#") );
+		request.path = urlDecode( parser.textFrom(start) );
+		request.originalPath = request.path;
+	}
+
+	private void parseQuery() throws ParseException {
+		do {
+			int start = parser.currentIndex();
+			while( queryChar() );
+			String name = urlDecode( parser.textFrom(start) );
+			String value = null;
+			if( parser.match('=') ) {
+				start = parser.currentIndex();
+				while( queryChar() || parser.match('=') );
+				value = urlDecode( parser.textFrom(start) );
+			}
+			if( name.length() > 0 || value != null ) {
+				if( value==null )
+					value = "";
+				Util.add(request.parameters,name,value);
+			}
+		} while( parser.match('&') );
+	}
+
+	private boolean queryChar() {
+		return parser.noneOf("=&# \t\n\f\r\u000b");
+	}
+
+	private void parseProtocol() throws ParseException {
+		int start = parser.currentIndex();
+		if( !(
+			parser.match("HTTP/")
+			&& parser.inCharRange('0','9')
+			&& parser.match('.')
+			&& parser.inCharRange('0','9')
+		) )
+			throw new ParseException(parser,"bad protocol");
+		request.protocol = parser.textFrom(start);
+		request.scheme = "http";
+	}
+
+
+	private void parserHeaderField() throws ParseException {
+		String name = parseName();
+		require( parser.match(':') );
+		while( parser.anyOf(" \t") );
+		String value = parseValue();
+		while( parser.anyOf(" \t") );
+		require( parser.match("\r\n") );
+		Util.add(request.headers,name,value);
+	}
+
+	private String parseName() throws ParseException {
+		int start = parser.currentIndex();
+		require( tokenChar() );
+		while( tokenChar() );
+		return parser.textFrom(start).toLowerCase();
+	}
+
+	private String parseValue() throws ParseException {
+		int start = parser.currentIndex();
+		while( !testEndOfValue() )
+			require( parser.anyChar() );
+		return parser.textFrom(start);
+	}
+
+	private boolean testEndOfValue() {
+		parser.begin();
+		while( parser.anyOf(" \t") );
+		boolean b = parser.endOfInput() || parser.anyOf("\r\n");
+		parser.failure();  // rollback
+		return b;
+	}
+
+	private void require(boolean b) throws ParseException {
+		if( !b )
+			throw new ParseException(parser,"failed");
+	}
+
+	boolean tokenChar() {
+		if( parser.endOfInput() )
+			return false;
+		char c = parser.currentChar();
+		if( 32 <= c && c <= 126 && "()<>@,;:\\\"/[]?={} \t\r\n".indexOf(c) == -1 ) {
+			parser.anyChar();
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+
+	private void parseCookies() throws ParseException {
+		String text = (String)request.headers.get("cookie");
+		if( text == null )
+			return;
+		this.parser = new Parser(text);
+		while(true) {
+			int start = parser.currentIndex();
+			while( parser.noneOf("=;") );
+			String name = urlDecode( parser.textFrom(start) );
+			if( parser.match('=') ) {
+				start = parser.currentIndex();
+				while( parser.noneOf(";") );
+				String value = parser.textFrom(start);
+				int len = value.length();
+				if( value.charAt(0)=='"' && value.charAt(len-1)=='"' )
+					value = value.substring(1,len-1);
+				value = urlDecode(value);
+				request.cookies.put(name,value);
+			}
+			if( parser.endOfInput() )
+				return;
+			require( parser.match(';') );
+			parser.match(' ');  // optional for bad browsers
+		}
+	}
+
+
+	private static final String contentTypeStart = "multipart/form-data; boundary=";
+
+	void parseMultipart() throws ParseException, UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		String contentType = (String)request.headers.get("content-type");
+		if( !contentType.startsWith(contentTypeStart) )
+			throw new RuntimeException(contentType);
+		String boundary = "--"+contentType.substring(contentTypeStart.length());
+		this.parser = new Parser(Util.toString(request.body,null));
+//System.out.println(this.parser.text);
+		require( parser.match(boundary) );
+		boundary = "\r\n" + boundary;
+		while( !parser.match("--\r\n") ) {
+			require( parser.match("\r\n") );
+			require( parser.match("Content-Disposition: form-data; name=") );
+			String name = quotedString();
+			String filename = null;
+			boolean isBinary = false;
+			if( parser.match("; filename=") ) {
+				filename = quotedString();
+				require( parser.match("\r\n") );
+				require( parser.match("Content-Type: ") );
+				int start = parser.currentIndex();
+				if( parser.match("application/") ) {
+					isBinary = true;
+				} else if( parser.match("image/") ) {
+					isBinary = true;
+				} else if( parser.match("text/") ) {
+					isBinary = false;
+				} else
+					throw new ParseException(parser,"bad file content-type");
+				while( parser.inCharRange('a','z') || parser.anyOf("-.") );
+				contentType = parser.textFrom(start);
+			}
+			require( parser.match("\r\n") );
+			require( parser.match("\r\n") );
+			int start = parser.currentIndex();
+			while( !parser.test(boundary) ) {
+				require( parser.anyChar() );
+			}
+			String value = parser.textFrom(start);
+			if( filename == null ) {
+				Util.add(request.parameters,name,value);
+			} else {
+				Object content = isBinary ? Util.toBytes(value) : value;
+				Request.MultipartFile mf = new Request.MultipartFile(filename,contentType,content);
+				Util.add(request.parameters,name,mf);
+			}
+			require( parser.match(boundary) );
+		}
+	}
+
+	private String quotedString() throws ParseException {
+		StringBuilder sb = new StringBuilder();
+		require( parser.match('"') );
+		while( !parser.match('"') ) {
+			if( parser.match("\\\"") ) {
+				sb.append('"');
+			} else {
+				require( parser.anyChar() );
+				sb.append( parser.lastChar() );
+			}
+		}
+		return sb.toString();
+	}
+
+	private String urlDecode(String s) throws ParseException {
+		try {
+			return URLDecoder.decode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			parser.rollback();
+			throw new ParseException(parser,e);
+		} catch(IllegalArgumentException e) {
+			parser.rollback();
+			throw new ParseException(parser,e);
+		}
+	}
+
+	// improve later
+	void parseJson() throws UnsupportedEncodingException {
+		if( request.body == null ) {
+			logger.warn("body is null\n"+request.rawHead);
+			return;
+		}
+		String contentType = (String)request.headers.get("content-type");
+		if( !contentType.equals("application/json; charset=utf-8") )
+			throw new RuntimeException(contentType);
+		String value = new String(request.body,"utf-8");
+		Util.add(request.parameters,"json",value);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Response.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,85 @@
+package goodjava.webserver;
+
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Collections;
+import java.util.List;
+
+
+public class Response {
+	public final String protocol = "HTTP/1.1";
+	public volatile Status status = Status.OK;
+	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	{
+		headers.put("server","Goodjava");
+	}
+	private static final Body empty = new Body(0,new InputStream(){
+		public int read() { return -1; }
+	});
+	public volatile Body body = empty;
+
+	public static class Body {
+		public final long length;
+		public final InputStream content;
+	
+		public Body(long length,InputStream content) {
+			this.length = length;
+			this.content = content;
+		}
+	}
+
+
+	public void addHeader(String name,String value) {
+		Util.add(headers,name,value);
+	}
+
+	public void setCookie(String name,String value,Map<String,String> attributes) {
+		StringBuilder buf = new StringBuilder();
+		buf.append( Util.urlEncode(name) );
+		buf.append( '=' );
+		buf.append( Util.urlEncode(value) );
+		for( Map.Entry<String,String> entry : attributes.entrySet() ) {
+			buf.append( "; " );
+			buf.append( entry.getKey() );
+			buf.append( '=' );
+			buf.append( entry.getValue() );
+		}
+		addHeader( "Set-Cookie", buf.toString() );
+	}
+
+
+	public String toHeaderString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append( protocol )
+			.append( ' ' ).append( status.code )
+			.append( ' ' ).append( status.reason )
+			.append( "\r\n" )
+		;
+		for( Map.Entry<String,Object> entry : headers.entrySet() ) {
+			String name = entry.getKey();
+			Object value = entry.getValue();
+			if( value instanceof List ) {
+				for( Object v : (List)value ) {
+					sb.append( name ).append( ": " ).append( v ).append( "\r\n" );
+				}
+			} else {
+				sb.append( name ).append( ": " ).append( value ).append( "\r\n" );
+			}
+		}
+		sb.append( "\r\n" );
+		return sb.toString();
+	}
+
+
+	public static Response errorResponse(Status status,String text) {
+		Response response = new Response();
+		response.status = status;
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
+		writer.write( text );
+		writer.close();
+		return response;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/ResponseOutputStream.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,22 @@
+package goodjava.webserver;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+
+// plenty of room for improvement
+public class ResponseOutputStream extends ByteArrayOutputStream {
+	private final Response response;
+
+	public ResponseOutputStream(Response response) {
+		if(response==null) throw new NullPointerException();
+		this.response = response;
+	}
+
+	@Override public void close() throws IOException {
+		super.close();
+		int size = size();
+		response.body = new Response.Body( size, new ByteArrayInputStream(buf,0,size) );
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Server.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,78 @@
+package goodjava.webserver;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+
+
+public class Server {
+	private static final Logger logger = LoggerFactory.getLogger(Server.class);
+
+	public final int port;
+	public final Handler handler;
+	public static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor)Executors.newCachedThreadPool();
+
+	public Server(int port,Handler handler) {
+		this.port = port;
+		this.handler = handler;
+	}
+
+	protected ServerSocket newServerSocket() throws IOException {
+		return new ServerSocket(port);
+	}
+
+	public synchronized void start() throws IOException {
+		final ServerSocket ss = newServerSocket();
+		threadPool.execute(new Runnable(){public void run() {
+			try {
+				while(!threadPool.isShutdown()) {
+					final Socket socket = ss.accept();
+					threadPool.execute(new Runnable(){public void run() {
+						Connection.handle(Server.this,socket);
+					}});
+				}
+			} catch(IOException e) {
+				logger.error("",e);
+			}
+		}});
+		logger.info("started server on port "+port);
+	}
+
+	public synchronized boolean stop(long timeoutSeconds) {
+		try {
+			threadPool.shutdownNow();
+			boolean stopped = threadPool.awaitTermination(timeoutSeconds,TimeUnit.SECONDS);
+			if(stopped)
+				logger.info("stopped server on port "+port);
+			else
+				logger.warn("couldn't stop server on port "+port);
+			return stopped;
+		} catch(InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static class ForAddress extends Server {
+		private final InetAddress addr;
+
+		public ForAddress(InetAddress addr,int port,Handler handler) {
+			super(port,handler);
+			this.addr = addr;
+		}
+
+		public ForAddress(String addrName,int port,Handler handler) throws UnknownHostException {
+			this(InetAddress.getByName(addrName),port,handler);
+		}
+
+		protected ServerSocket newServerSocket() throws IOException {
+			return new ServerSocket(port,0,addr);
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Status.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,43 @@
+package goodjava.webserver;
+
+import java.util.Map;
+import java.util.HashMap;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+
+
+public class Status {
+	private static final Logger logger = LoggerFactory.getLogger(Status.class);
+
+	public final int code;
+	public final String reason;
+
+	public Status(int code,String reason) {
+		this.code = code;
+		this.reason = reason;
+	}
+
+	private static final Map<Integer,Status> map = new HashMap<Integer,Status>();
+
+	protected static Status newStatus(int code,String reason) {
+		Status status = new Status(code,reason);
+		map.put(code,status);
+		return status;
+	}
+
+	public static Status getStatus(int code) {
+		Status status = map.get(code);
+		if( status == null ) {
+			logger.warn("missing status "+code);
+			status = new Status(code,"");
+		}
+		return status;
+	}
+
+	public static final Status OK = newStatus(200,"OK");
+	public static final Status MOVED_PERMANENTLY = newStatus(301,"Moved Permanently");
+	public static final Status FOUND = newStatus(302,"Found");
+	public static final Status BAD_REQUEST = newStatus(400,"Bad Request");
+	public static final Status NOT_FOUND = newStatus(404,"Not Found");
+	public static final Status INTERNAL_SERVER_ERROR = newStatus(500,"Internal Server Error");
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/Util.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,55 @@
+package goodjava.webserver;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+
+final class Util {
+
+	static String urlEncode(String s) {
+		try {
+			return URLEncoder.encode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	static void add(Map<String,Object> map,String name,Object value) {
+		Object current = map.get(name);
+		if( current == null ) {
+			map.put(name,value);
+		} else if( current instanceof List ) {
+			List list = (List)current;
+			list.add(value);
+		} else {
+			List list = new ArrayList();
+			list.add(current);
+			list.add(value);
+			map.put(name,list);
+		}
+	}
+
+	static String toString(byte[] a,String charset) throws UnsupportedEncodingException {
+		if( charset != null )
+			return new String(a,charset);
+		char[] ac = new char[a.length];
+		for( int i=0; i<a.length; i++ ) {
+			ac[i] = (char)a[i];
+		}
+		return new String(ac);
+	}
+
+	static byte[] toBytes(String s) {
+		char[] ac = s.toCharArray();
+		byte[] a = new byte[ac.length];
+		for( int i=0; i<ac.length; i++ ) {
+			a[i] = (byte)ac[i];
+		}
+		return a;
+	}
+
+	private Util() {}  // never
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/Cookies.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,42 @@
+package goodjava.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+
+
+public final class Cookies implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		String name = (String)request.parameters.get("name");
+		if( name != null ) {
+			Map<String,String> attributes = new HashMap<String,String>();
+			String value = (String)request.parameters.get("value");
+			if( value != null ) {
+				response.setCookie(name,value,attributes);
+			} else {
+				attributes.put("Max-Age","0");
+				response.setCookie(name,"delete",attributes);
+			}
+		}
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,String> entry : request.cookies.entrySet() ) {
+				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/Example.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,72 @@
+package goodjava.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import org.apache.log4j.EnhancedPatternLayout;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+import goodjava.webserver.Server;
+import goodjava.webserver.handlers.MapHandler;
+import goodjava.webserver.handlers.SafeHandler;
+import goodjava.webserver.handlers.LogHandler;
+import goodjava.webserver.handlers.FileHandler;
+import goodjava.webserver.handlers.DirHandler;
+import goodjava.webserver.handlers.ListHandler;
+import goodjava.webserver.handlers.ContentTypeHandler;
+
+
+public class Example implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			writer.write("Hello World\n");
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException("shouldn't happen",e);
+		}
+		return response;
+	}
+
+	public static void simple() throws IOException {
+		Handler handler = new Example();
+		new Server(8080,handler).start();
+	}
+
+	public static void fancy() throws IOException {
+		Map<String,Handler> map = new HashMap<String,Handler>();
+		map.put( "/hello", new Example() );
+		map.put( "/headers", new Headers() );
+		map.put( "/params", new Params() );
+		map.put( "/cookies", new Cookies() );
+		Handler mapHandler = new MapHandler(map);
+		FileHandler fileHandler = new FileHandler();
+		Handler dirHandler = new DirHandler(fileHandler);
+		Handler handler = new ListHandler( mapHandler, fileHandler, dirHandler );
+		handler = new ContentTypeHandler(handler);
+		handler = new SafeHandler(handler);
+		handler = new LogHandler(handler);
+		new Server(8080,handler).start();
+	}
+
+	public static void initLogging() {
+//		Logger.getRootLogger().setLevel(Level.INFO);
+		EnhancedPatternLayout layout = new EnhancedPatternLayout("%d{HH:mm:ss} %-5p %c - %m%n");
+		ConsoleAppender appender = new ConsoleAppender(layout,"System.err");
+		Logger.getRootLogger().addAppender(appender);
+	}
+
+	public static void main(String[] args) throws Exception {
+		initLogging();
+		fancy();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/Headers.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,30 @@
+package goodjava.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+
+
+public final class Headers implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,Object> entry : request.headers.entrySet() ) {
+				writer.write(entry.getKey()+": "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/Params.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,30 @@
+package goodjava.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Map;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+
+
+public final class Params implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		try {
+			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+			for( Map.Entry<String,Object> entry : request.parameters.entrySet() ) {
+				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
+			}
+			writer.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		return response;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/post.html	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+	<body>
+		<form action=/params method=post>
+			<p>a <input name=a></p>
+			<p>b <input name=b></p>
+			<p><input type=submit></p>
+		</form>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/post_multipart.html	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+	<body>
+		<form action=/params method=post enctype="multipart/form-data">
+			<p>a <input name=a></p>
+			<p>b <input name=b></p>
+			<p><input type=file name=file></p>
+			<p><input type=submit></p>
+		</form>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/ContentTypeHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,55 @@
+package goodjava.webserver.handlers;
+
+import java.util.Map;
+import java.util.HashMap;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public class ContentTypeHandler implements Handler {
+	private final Handler handler;
+
+	// maps extension to content-type
+	// key must be lower case
+	public final Map<String,String> map = new HashMap<String,String>();
+
+	// set to null for none
+	public String contentTypeForNoExtension;
+
+	public ContentTypeHandler(Handler handler) {
+		this(handler,"utf-8");
+	}
+
+	public ContentTypeHandler(Handler handler,String charset) {
+		this.handler = handler;
+		String attrs = charset== null ? "" : "; charset="+charset;
+		String htmlType = "text/html" + attrs;
+		String textType = "text/plain" + attrs;
+		contentTypeForNoExtension = htmlType;
+		map.put( "html", htmlType );
+		map.put( "txt", textType );
+		map.put( "css", "text/css" );
+		map.put( "mp4", "video/mp4" );
+		// add more as need
+	}
+
+	public Response handle(Request request) {
+		Response response = handler.handle(request);
+		if( response!=null && !response.headers.containsKey("content-type") ) {
+			String path = request.path;
+			int iSlash = path.lastIndexOf('/');
+			int iDot = path.lastIndexOf('.');
+			String type;
+			if( iDot < iSlash ) {  // no extension
+				type = contentTypeForNoExtension;
+			} else {  // extension
+				String extension = path.substring(iDot+1);
+				type = map.get( extension.toLowerCase() );
+			}
+			if( type != null )
+				response.headers.put("content-type",type);
+		}
+		return response;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/DirHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,68 @@
+package goodjava.webserver.handlers;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+
+
+public final class DirHandler implements Handler {
+	private final FileHandler fileHandler;
+
+	public DirHandler(FileHandler fileHandler) {
+		this.fileHandler = fileHandler;
+	}
+
+	private static final Comparator<File> sorter = new Comparator<File>() {
+		public int compare(File f1, File f2) {
+			return f1.getName().compareTo(f2.getName());
+		}
+	};
+
+	public Response handle(Request request) {
+		try {
+			File file = fileHandler.file(request);
+			if( request.path.endsWith("/") && file.isDirectory() ) {
+				DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
+				Response response = new Response();
+				response.headers.put( "content-type", "text/html; charset=utf-8" );
+				Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
+				writer.write( "<!doctype html><html>" );
+				writer.write( "<head><style>td{padding: 2px 8px}</style></head>" );
+				writer.write( "<body>" );
+				writer.write( "<h1>Directory: "+request.path+"</h1>" );
+				writer.write( "<table border=0>" );
+				File[] a = file.listFiles();
+				Arrays.sort(a,sorter);
+				for( File child : a ) {
+					String name = child.getName();
+					if( child.isDirectory() )
+						name += '/';
+					writer.write( "<tr>" );
+					writer.write( "<td><a href='"+name+"'>"+name+"</a></td>" );
+					writer.write( "<td>"+child.length()+" bytes</td>" );
+					writer.write( "<td>"+fmt.format(new Date(child.lastModified()))+"</td>" );
+					writer.write( "</tr>" );
+				}
+				writer.write( "</table>" );
+				writer.write( "</body>" );
+				writer.write( "</html>" );
+				writer.close();
+				return response;
+			}
+			return null;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/DomainHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,81 @@
+package goodjava.webserver.handlers;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.ref.Reference;
+//import java.lang.ref.WeakReference;
+import java.lang.ref.SoftReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.Map;
+import java.util.HashMap;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public final class DomainHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger(DomainHandler.class);
+
+	public interface Factory {
+		public Handler newHandler(String domain);
+	}
+
+	private static void close(Handler handler) {
+		if( handler instanceof Closeable ) {
+			try {
+				((Closeable)handler).close();
+			} catch(IOException e) {
+				logger.error(handler.toString(),e);
+			}
+		}
+	}
+
+	private final Map<String,Reference<Handler>> map = new HashMap<String,Reference<Handler>>();
+
+	private final Factory factory;
+
+	public DomainHandler(Factory factory) {
+		this.factory = factory;
+	}
+
+	public Response handle(Request request) {
+		String host = (String)request.headers.get("host");
+		if( host == null )
+			return null;
+		int i = host.indexOf(':');
+		String domain = i == -1 ? host : host.substring(0,i);
+		Handler handler = getHandler(domain);
+		return handler==null ? null : handler.handle(request);
+	}
+
+	public Handler getHandler(String domain) {
+		domain = domain.toLowerCase();
+		synchronized(map) {
+			Reference<Handler> ref = map.get(domain);
+			Handler handler = ref==null ? null : ref.get();
+			if( handler == null ) {
+				//if(ref!=null) logger.info("gc "+domain);
+				handler = factory.newHandler(domain);
+				if( handler == null )
+					return null;
+				map.put(domain,new SoftReference<Handler>(handler));
+			}
+			return handler;
+		}
+	}
+
+	public void removeHandler(String domain) {
+		logger.info("removeHandler "+domain);
+		domain = domain.toLowerCase();
+		synchronized(map) {
+			Reference<Handler> ref = map.remove(domain);
+			Handler handler = ref==null ? null : ref.get();
+			if( handler != null ) {
+				close(handler);
+			}
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/FileHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,51 @@
+package goodjava.webserver.handlers;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+
+
+public class FileHandler implements Handler {
+	final File dir;
+
+	public FileHandler() {
+		this(".");
+	}
+
+	public FileHandler(String pathname) {
+		this(new File(pathname));
+	}
+
+	public FileHandler(File dir) {
+		if( !dir.isDirectory() )
+			throw new RuntimeException("must be a directory");
+		this.dir = dir;
+	}
+
+	File file(Request request) {
+		return new File(dir,request.path);
+	}
+
+	public Response handle(Request request) {
+		try {
+			File file = file(request);
+			if( file.isFile() ) {
+				Response response = new Response();
+				response.body = new Response.Body( file.length(), new FileInputStream(file) );
+				return response;
+			}
+			return null;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/IndexHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,33 @@
+package goodjava.webserver.handlers;
+
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public final class IndexHandler implements Handler {
+	private final Handler handler;
+	private final String indexName;
+
+	public IndexHandler(Handler handler) {
+		this(handler,"index.html");
+	}
+
+	public IndexHandler(Handler handler,String indexName) {
+		this.handler = handler;
+		this.indexName = indexName;
+	}
+
+	public Response handle(Request request) {
+		if( request.path.endsWith("/") ) {
+			String path = request.path;
+			try {
+				request.path += indexName;
+				return handler.handle(request);
+			} finally {
+				request.path = path;
+			}
+		} else
+			return handler.handle(request);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/ListHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,23 @@
+package goodjava.webserver.handlers;
+
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public final class ListHandler implements Handler {
+	private final Handler[] handlers;
+
+	public ListHandler(Handler... handlers) {
+		this.handlers = handlers;
+	}
+
+	public Response handle(Request request) {
+		for( Handler handler : handlers ) {
+			Response response = handler.handle(request);
+			if( response != null )
+				return response;
+		}
+		return null;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/LogHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,24 @@
+package goodjava.webserver.handlers;
+
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public final class LogHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger("HTTP");
+
+	private final Handler handler;
+
+	public LogHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	public Response handle(Request request) {
+		Response response = handler.handle(request);
+		logger.info( request.method + " " + request.path + " " + response.status.code + " " + response.body.length );
+		return response;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/MapHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,20 @@
+package goodjava.webserver.handlers;
+
+import java.util.Map;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public final class MapHandler implements Handler {
+	private final Map<String,Handler> map;
+
+	public MapHandler(Map<String,Handler> map) {
+		this.map = map;
+	}
+
+	public Response handle(Request request) {
+		Handler handler = map.get(request.path);
+		return handler==null ? null : handler.handle(request);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/SafeHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -0,0 +1,44 @@
+package goodjava.webserver.handlers;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.IOException;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ResponseOutputStream;
+import goodjava.webserver.Status;
+
+
+public final class SafeHandler implements Handler {
+	private static final Logger logger = LoggerFactory.getLogger(SafeHandler.class);
+
+	private final Handler handler;
+
+	public SafeHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	public Response handle(Request request) {
+		try {
+			Response response = handler.handle(request);
+			if( response != null )
+				return response;
+		} catch(RuntimeException e) {
+			logger.error("",e);
+			Response response = new Response();
+			response.status = Status.INTERNAL_SERVER_ERROR;
+			response.headers.put( "content-type", "text/plain; charset=utf-8" );
+			PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
+			writer.write( "Internel Server Error\n\n" );
+			e.printStackTrace(writer);
+			writer.close();
+			return response;
+		}
+		return Response.errorResponse( Status.NOT_FOUND, request.path+" not found\n" );
+	}
+
+}
--- a/src/luan/Luan.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/Luan.java	Tue Sep 17 01:35:01 2019 -0400
@@ -9,8 +9,8 @@
 import java.util.Iterator;
 import java.util.Arrays;
 import java.util.Set;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 import luan.modules.BasicLuan;
 import luan.modules.JavaLuan;
 import luan.modules.PackageLuan;
--- a/src/luan/host/WebHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/host/WebHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -1,12 +1,12 @@
 package luan.host;
 
 import java.io.File;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.handlers.DomainHandler;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.handlers.DomainHandler;
 import luan.Luan;
 import luan.LuanException;
 import luan.LuanTable;
--- a/src/luan/host/run.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/host/run.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -8,7 +8,7 @@
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "run"
 local NotFound = require "java:luan.modules.http.NotFound"
-local ListHandler = require "java:luan.lib.webserver.handlers.ListHandler"
+local ListHandler = require "java:goodjava.webserver.handlers.ListHandler"
 local WebHandler = require "java:luan.host.WebHandler"
 Hosting.WebHandler = WebHandler
 
@@ -21,10 +21,10 @@
 
 -- web server
 
-local Server = require "java:luan.lib.webserver.Server"
-local IndexHandler = require "java:luan.lib.webserver.handlers.IndexHandler"
-local ContentTypeHandler = require "java:luan.lib.webserver.handlers.ContentTypeHandler"
-local SafeHandler = require "java:luan.lib.webserver.handlers.SafeHandler"
+local Server = require "java:goodjava.webserver.Server"
+local IndexHandler = require "java:goodjava.webserver.handlers.IndexHandler"
+local ContentTypeHandler = require "java:goodjava.webserver.handlers.ContentTypeHandler"
+local SafeHandler = require "java:goodjava.webserver.handlers.SafeHandler"
 
 local handler = WebHandler.new(Hosting.sites_dir)
 local not_found_hander = NotFound.new(handler)
--- a/src/luan/lib/json/JsonParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,216 +0,0 @@
-package luan.lib.json;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Collections;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
-
-
-public final class JsonParser {
-
-	public static Object parse(String text) throws ParseException {
-		return new JsonParser(text).parse();
-	}
-
-	private final Parser parser;
-
-	private JsonParser(String text) {
-		this.parser = new Parser(text);
-	}
-
-	private ParseException exception(String msg) {
-		return new ParseException(parser,msg);
-	}
-
-	private Object parse() throws ParseException {
-		spaces();
-		Object value = value();
-		spaces();
-		if( !parser.endOfInput() )
-			throw exception("unexpected text");
-		return value;
-	}
-
-	private Object value() throws ParseException {
-		if( parser.match("null") )
-			return null;
-		if( parser.match("true") )
-			return Boolean.TRUE;
-		if( parser.match("false") )
-			return Boolean.FALSE;
-		String s = string();
-		if( s != null )
-			return s;
-		Number n = number();
-		if( n != null )
-			return n;
-		List a = array();
-		if( a != null )
-			return a;
-		Map o = object();
-		if( o != null )
-			return o;
-		throw exception("invalid value");
-	}
-
-	private String string() throws ParseException {
-		parser.begin();
-		if( !parser.match('"') )
-			return parser.failure(null);
-		StringBuilder sb = new StringBuilder();
-		while( parser.anyChar() ) {
-			char c = parser.lastChar();
-			switch(c) {
-			case '"':
-				return parser.success(sb.toString());
-			case '\\':
-				if( parser.anyChar() ) {
-					c = parser.lastChar();
-					switch(c) {
-					case '"':
-					case '\'':  // not in spec
-					case '\\':
-					case '/':
-						sb.append(c);
-						continue;
-					case 'b':
-						sb.append('\b');
-						continue;
-					case 'f':
-						sb.append('\f');
-						continue;
-					case 'n':
-						sb.append('\n');
-						continue;
-					case 'r':
-						sb.append('\r');
-						continue;
-					case 't':
-						sb.append('\t');
-						continue;
-					case 'u':
-						int n = 0;
-						for( int i=0; i<4; i++ ) {
-							int d;
-							if( parser.inCharRange('0','9') ) {
-								d = parser.lastChar() - '0';
-							} else if( parser.inCharRange('a','f') ) {
-								d = parser.lastChar() - 'a' + 10;
-							} else if( parser.inCharRange('A','F') ) {
-								d = parser.lastChar() - 'A' + 10;
-							} else {
-								throw exception("invalid hex digit");
-							}
-							n = 16*n + d;
-						}
-						sb.append((char)n);
-						continue;
-					}
-				}
-				throw exception("invalid escape char");
-			default:
-				sb.append(c);
-			}
-		}
-		parser.failure();
-		throw exception("unclosed string");
-	}
-
-	private Number number() {
-		int start = parser.begin();
-		boolean isFloat = false;
-		parser.match('-');
-		if( !parser.match('0') ) {
-			if( !parser.inCharRange('1','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-		}
-		if( parser.match('.') ) {
-			if( !parser.inCharRange('0','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-			isFloat = true;
-		}
-		if( parser.anyOf("eE") ) {
-			parser.anyOf("+-");
-			if( !parser.inCharRange('0','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-			isFloat = true;
-		}
-		String s = parser.textFrom(start);
-		Number n;
-		if(isFloat)
-			n = Double.valueOf(s);
-		else
-			n = Long.valueOf(s);
-		return parser.success(n);
-	}
-
-	private List array() throws ParseException {
-		parser.begin();
-		if( !parser.match('[') )
-			return parser.failure(null);
-		spaces();
-		if( parser.match(']') )
-			return parser.success(Collections.emptyList());
-		List list = new ArrayList();
-		list.add( value() );
-		spaces();
-		while( parser.match(',') ) {
-			spaces();
-			list.add( value() );
-			spaces();
-		}
-		if( parser.match(']') )
-			return parser.success(list);
-		if( parser.endOfInput() ) {
-			parser.failure();
-			throw exception("unclosed array");
-		}
-		throw exception("unexpected text in array");
-	}
-
-	private Map object() throws ParseException {
-		parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		spaces();
-		if( parser.match('}') )
-			return parser.success(Collections.emptyMap());
-		Map map = new LinkedHashMap();
-		addEntry(map);
-		while( parser.match(',') ) {
-			spaces();
-			addEntry(map);
-		}
-		if( parser.match('}') )
-			return parser.success(map);
-		if( parser.endOfInput() ) {
-			parser.failure();
-			throw exception("unclosed object");
-		}
-		throw exception("unexpected text in object");
-	}
-
-	private void addEntry(Map map) throws ParseException {
-		String key = string();
-		if( key==null )
-			throw exception("invalid object key");
-		spaces();
-		if( !parser.match(':') )
-			throw exception("':' expected");
-		spaces();
-		Object value = value();
-		spaces();
-		map.put(key,value);
-	}
-
-	private void spaces() {
-		while( parser.anyOf(" \t\r\n") );
-	}
-
-}
--- a/src/luan/lib/json/JsonToString.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-package luan.lib.json;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Iterator;
-
-
-public class JsonToString {
-
-	public static final class JsonException extends RuntimeException {
-		private JsonException(String msg) {
-			super(msg);
-		}
-	}
-
-	public static String toString(Object obj) throws JsonException {
-		StringBuilder sb = new StringBuilder();
-		new JsonToString().toString(obj,sb,0);
-		sb.append('\n');
-		return sb.toString();
-	}
-
-	public static String toCompressedString(Object obj) throws JsonException {
-		StringBuilder sb = new StringBuilder();
-		JsonToString jts = new JsonToString() {
-			void indent(StringBuilder sb,int indented) {}
-		};
-		jts.toString(obj,sb,0);
-		return sb.toString();
-	}
-
-	private void toString(Object obj,StringBuilder sb,int indented) throws JsonException {
-		if( obj == null || obj instanceof Boolean || obj instanceof Number ) {
-			sb.append(obj);
-			return;
-		}
-		if( obj instanceof String ) {
-			toString((String)obj,sb);
-			return;
-		}
-		if( obj instanceof List ) {
-			toString((List)obj,sb,indented);
-			return;
-		}
-		if( obj instanceof Map ) {
-			toString((Map)obj,sb,indented);
-			return;
-		}
-		throw new JsonException("can't handle type "+obj.getClass().getName());
-	}
-
-	private static void toString(final String s,StringBuilder sb) {
-		sb.append('"');
-		for( int i=0; i<s.length(); i++ ) {
-			char c = s.charAt(i);
-			switch(c) {
-			case '"':
-				sb.append("\\\"");
-				break;
-			case '\\':
-				sb.append("\\\\");
-				break;
-			case '\b':
-				sb.append("\\b");
-				break;
-			case '\f':
-				sb.append("\\f");
-				break;
-			case '\n':
-				sb.append("\\n");
-				break;
-			case '\r':
-				sb.append("\\r");
-				break;
-			case '\t':
-				sb.append("\\t");
-				break;
-			default:
-				sb.append(c);
-			}
-		}
-		sb.append('"');
-	}
-
-	public static String javascriptEncode(String s) {
-		StringBuilder sb = new StringBuilder();
-		for( int i=0; i<s.length(); i++ ) {
-			char c = s.charAt(i);
-			switch(c) {
-			case '"':
-				sb.append("\\\"");
-				break;
-			case '\'':  // added for javascript
-				sb.append("\\'");
-				break;
-			case '\\':
-				sb.append("\\\\");
-				break;
-			case '\b':
-				sb.append("\\b");
-				break;
-			case '\f':
-				sb.append("\\f");
-				break;
-			case '\n':
-				sb.append("\\n");
-				break;
-			case '\r':
-				sb.append("\\r");
-				break;
-			case '\t':
-				sb.append("\\t");
-				break;
-			default:
-				sb.append(c);
-			}
-		}
-		return sb.toString();
-	}
-
-	private void toString(List list,StringBuilder sb,int indented) {
-		sb.append('[');
-		if( !list.isEmpty() ) {
-			indent(sb,indented+1);
-			toString(list.get(0),sb,indented+1);
-			for( int i=1; i<list.size(); i++ ) {
-				sb.append(", ");
-				toString(list.get(i),sb,indented+1);
-			}
-			indent(sb,indented);
-		}
-		sb.append(']');
-		return;
-	}
-
-	private void toString(Map map,StringBuilder sb,int indented) throws JsonException {
-		sb.append('{');
-		if( !map.isEmpty() ) {
-			Iterator<Map.Entry> i = map.entrySet().iterator();
-			indent(sb,indented+1);
-			toString(i.next(),sb,indented+1);
-			while( i.hasNext() ) {
-				sb.append(',');
-				indent(sb,indented+1);
-				toString(i.next(),sb,indented+1);
-			}
-			indent(sb,indented);
-		}
-		sb.append('}');
-	}
-
-	private void toString(Map.Entry entry,StringBuilder sb,int indented) throws JsonException {
-		Object key = entry.getKey();
-		if( !(key instanceof String) )
-			throw new JsonException("table keys must be strings");
-		toString((String)key,sb);
-		sb.append(": ");
-		toString(entry.getValue(),sb,indented);
-	}
-
-	void indent(StringBuilder sb,int indented) {
-		sb.append('\n');
-		for( int i=0; i<indented; i++ ) {
-			sb.append('\t');
-		}
-	}
-
-	private JsonToString() {}
-}
--- a/src/luan/lib/logging/Log4jFactory.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-package luan.lib.logging;
-
-
-public final class Log4jFactory extends LoggerFactory {
-	private static final class Log4jLogger implements Logger {
-		final org.apache.log4j.Logger log4j;
-
-		Log4jLogger(org.apache.log4j.Logger log4j) {
-			this.log4j = log4j;
-		}
-
-		@Override public void error(String msg) {
-			log4j.error(msg);
-		}
-
-		@Override public void error(String msg,Throwable t) {
-			log4j.error(msg,t);
-		}
-
-		@Override public void warn(String msg) {
-			log4j.warn(msg);
-		}
-
-		@Override public void warn(String msg,Throwable t) {
-			log4j.warn(msg,t);
-		}
-
-		@Override public void info(String msg) {
-			log4j.info(msg);
-		}
-
-		@Override public void info(String msg,Throwable t) {
-			log4j.info(msg,t);
-		}
-
-		@Override public void debug(String msg) {
-			log4j.debug(msg);
-		}
-
-		@Override public void debug(String msg,Throwable t) {
-			log4j.debug(msg,t);
-		}
-	}
-
-	@Override protected Logger getLoggerImpl(Class cls) {
-		return new Log4jLogger(org.apache.log4j.Logger.getLogger(cls));
-	}
-
-	@Override protected Logger getLoggerImpl(String name) {
-		return new Log4jLogger(org.apache.log4j.Logger.getLogger(name));
-	}
-}
--- a/src/luan/lib/logging/Logger.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-package luan.lib.logging;
-
-// Because slf4j is an overcomplicated mess that caches loggers when it shouldn't.
-
-public interface Logger {
-	public void error(String msg);
-	public void error(String msg,Throwable t);
-	public void warn(String msg);
-	public void warn(String msg,Throwable t);
-	public void info(String msg);
-	public void info(String msg,Throwable t);
-	public void debug(String msg);
-	public void debug(String msg,Throwable t);
-}
--- a/src/luan/lib/logging/LoggerFactory.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-package luan.lib.logging;
-
-
-public abstract class LoggerFactory {
-	public static LoggerFactory implementation = new Log4jFactory();
-
-	protected abstract Logger getLoggerImpl(Class cls);
-	protected abstract Logger getLoggerImpl(String name);
-
-	public static Logger getLogger(Class cls) {
-		return implementation.getLoggerImpl(cls);
-	}
-
-	public static Logger getLogger(String name) {
-		return implementation.getLoggerImpl(name);
-	}
-}
--- a/src/luan/lib/parser/ParseException.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-package luan.lib.parser;
-
-
-public final class ParseException extends Exception {
-	public final String text;
-	public final int errorIndex;
-	public final int highIndex;
-
-	public ParseException(Parser parser,String msg) {
-		super(msg);
-		this.text = parser.text;
-		this.errorIndex = parser.currentIndex();
-		this.highIndex = parser.highIndex();
-	}
-
-	public ParseException(Parser parser,Exception cause) {
-		this(parser,cause.getMessage(),cause);
-	}
-
-	public ParseException(Parser parser,String msg,Exception cause) {
-		super(msg,cause);
-		this.text = parser.text;
-		this.errorIndex = parser.currentIndex();
-		this.highIndex = parser.highIndex();
-	}
-
-	private class Location {
-		final int line;
-		final int pos;
-
-		Location(int index) {
-			int line = 0;
-			int i = -1;
-			while(true) {
-				int j = text.indexOf('\n',i+1);
-				if( j == -1 || j >= index )
-					break;
-				i = j;
-				line++;
-			}
-			this.line = line;
-			this.pos = index - i - 1;
-		}
-	}
-
-	private String[] lines() {
-		return text.split("\n",-1);
-	}
-
-	@Override public String getMessage() {
-		String line;
-		int pos;
-		StringBuilder sb = new StringBuilder(super.getMessage());
-		if( text.indexOf('\n') == -1 ) {
-			line = text;
-			pos = errorIndex;
-			sb.append( " (position " + (pos+1) + ")\n" );
-		} else {
-			Location loc = new Location(errorIndex);
-			line = lines()[loc.line];
-			pos = loc.pos;
-			sb.append( " (line " + (loc.line+1) + ", pos " + (pos+1) + ")\n" );
-		}
-		sb.append( line + "\n" );
-		for( int i=0; i<pos; i++ ) {
-			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
-		}
-		sb.append("^\n");
-		return sb.toString();
-	}
-}
--- a/src/luan/lib/parser/Parser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-package luan.lib.parser;
-
-
-public class Parser {
-	public final String text;
-	private final int len;
-	private int[] stack = new int[256];
-	private int frame = 0;
-	private int iHigh;
-
-	public Parser(String text) {
-		this.text = text;
-		this.len = text.length();
-	}
-
-	private int i() {
-		return stack[frame];
-	}
-
-	private void i(int i) {
-		stack[frame] += i;
-		if( iHigh < stack[frame] )
-			iHigh = stack[frame];
-	}
-
-	public int begin() {
-		frame++;
-		if( frame == stack.length ) {
-			int[] a = new int[2*frame];
-			System.arraycopy(stack,0,a,0,frame);
-			stack = a;
-		}
-		stack[frame] = stack[frame-1];
-		return i();
-	}
-
-	public void rollback() {
-		stack[frame] = frame==0 ? 0 : stack[frame-1];
-	}
-
-	public <T> T success(T t) {
-		success();
-		return t;
-	}
-
-	public boolean success() {
-		frame--;
-		stack[frame] = stack[frame+1];
-		return true;
-	}
-
-	public <T> T failure(T t) {
-		failure();
-		return t;
-	}
-
-	public boolean failure() {
-		frame--;
-		return false;
-	}
-
-	public int currentIndex() {
-		return i();
-	}
-/*
-	public int errorIndex() {
-		return frame > 0 ? stack[frame-1] : 0;
-	}
-*/
-	public int highIndex() {
-		return iHigh;
-	}
-
-	public char lastChar() {
-		return text.charAt(i()-1);
-	}
-
-	public char currentChar() {
-		return text.charAt(i());
-	}
-
-	public boolean endOfInput() {
-		return i() >= len;
-	}
-
-	public boolean match(char c) {
-		if( endOfInput() || text.charAt(i()) != c )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean match(String s) {
-		int n = s.length();
-		if( !text.regionMatches(i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean matchIgnoreCase(String s) {
-		int n = s.length();
-		if( !text.regionMatches(true,i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean anyOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean noneOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean inCharRange(char cLow, char cHigh) {
-		if( endOfInput() )
-			return false;
-		char c = text.charAt(i());
-		if( !(cLow <= c && c <= cHigh) )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean anyChar() {
-		if( endOfInput() )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean test(char c) {
-		return !endOfInput() && text.charAt(i()) == c;
-	}
-
-	public boolean test(String s) {
-		return text.regionMatches(i(),s,0,s.length());
-	}
-
-	public boolean testIgnoreCase(String s) {
-		return text.regionMatches(true,i(),s,0,s.length());
-	}
-
-	public String textFrom(int start) {
-		return text.substring(start,i());
-	}
-
-}
--- a/src/luan/lib/queryparser/FieldParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-package luan.lib.queryparser;
-
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.SortField;
-import luan.lib.parser.ParseException;
-
-
-public interface FieldParser {
-	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException;
-	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException;
-	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException;
-}
--- a/src/luan/lib/queryparser/MultiFieldParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-package luan.lib.queryparser;
-
-import java.util.Map;
-import java.util.HashMap;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.SortField;
-import luan.lib.parser.ParseException;
-
-
-public class MultiFieldParser implements FieldParser {
-
-	/**
-	 * maps field name to FieldParser
-	 */
-	public final Map<String,FieldParser> fields = new HashMap<String,FieldParser>();
-	public boolean allowUnspecifiedFields = false;
-	private final FieldParser defaultFieldParser;
-	private final String[] defaultFields;
-
-	public MultiFieldParser() {
-		this.defaultFieldParser = null;
-		this.defaultFields = null;
-	}
-
-	public MultiFieldParser(FieldParser defaultFieldParser,String... defaultFields) {
-		this.defaultFieldParser = defaultFieldParser;
-		this.defaultFields = defaultFields;
-		for( String field : defaultFields ) {
-			fields.put(field,defaultFieldParser);
-		}
-	}
-
-	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		if( field == null ) {
-			if( defaultFieldParser == null )
-				throw qp.exception("no defaults were specified, so a field is required");
-			if( defaultFields.length == 1 )
-				return defaultFieldParser.getQuery(qp,defaultFields[0],query);
-			BooleanQuery bq = new BooleanQuery();
-			for( String f : defaultFields ) {
-				bq.add( defaultFieldParser.getQuery(qp,f,query), BooleanClause.Occur.SHOULD );
-			}
-			return bq;
-		} else {
-			FieldParser fp = fields.get(field);
-			if( fp != null )
-				return fp.getQuery(qp,field,query);
-			if( allowUnspecifiedFields )
-				return defaultFieldParser.getQuery(qp,field,query);
-			throw qp.exception("unrecognized field '"+field+"'");
-		}
-	}
-
-	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		if( field == null ) {
-			if( defaultFieldParser == null )
-				throw qp.exception("no defaults were specified, so a field is required");
-			if( defaultFields.length == 1 )
-				return defaultFieldParser.getRangeQuery(qp,defaultFields[0],minQuery,maxQuery,includeMin,includeMax);
-			BooleanQuery bq = new BooleanQuery();
-			for( String f : defaultFields ) {
-				bq.add( defaultFieldParser.getRangeQuery(qp,f,minQuery,maxQuery,includeMin,includeMax), BooleanClause.Occur.SHOULD );
-			}
-			return bq;
-		} else {
-			FieldParser fp = fields.get(field);
-			if( fp != null )
-				return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-			if( allowUnspecifiedFields )
-				return defaultFieldParser.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-			throw qp.exception("field '"+field+"' not specified");
-		}
-	}
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
-		FieldParser fp = fields.get(field);
-		if( fp != null )
-			return fp.getSortField(qp,field,reverse);
-		if( allowUnspecifiedFields )
-			return defaultFieldParser.getSortField(qp,field,reverse);
-		throw qp.exception("field '"+field+"' not specified");
-	}
-
-}
--- a/src/luan/lib/queryparser/NumberFieldParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-package luan.lib.queryparser;
-
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.NumericRangeQuery;
-import org.apache.lucene.search.SortField;
-import luan.lib.parser.ParseException;
-
-
-public abstract class NumberFieldParser implements FieldParser {
-
-	@Override public final Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		return getRangeQuery(qp,field,query,query,true,true);
-	}
-
-	@Override public final Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		try {
-			return getRangeQuery(field,minQuery,maxQuery,includeMin,includeMax);
-		} catch(NumberFormatException e) {
-			throw qp.exception(e);
-		}
-	}
-
-	abstract protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax);
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
-		return new SortField( field, sortType(), reverse );
-	}
-
-	abstract protected SortField.Type sortType();
-
-
-	public static final FieldParser INT = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			int min = Integer.parseInt(minQuery);
-			int max = Integer.parseInt(maxQuery);
-			return NumericRangeQuery.newIntRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.INT;
-		}
-	};
-
-	public static final FieldParser LONG = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			long min = Long.parseLong(minQuery);
-			long max = Long.parseLong(maxQuery);
-			return NumericRangeQuery.newLongRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.LONG;
-		}
-	};
-
-	public static final FieldParser FLOAT = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			float min = Float.parseFloat(minQuery);
-			float max = Float.parseFloat(maxQuery);
-			return NumericRangeQuery.newFloatRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.FLOAT;
-		}
-	};
-
-	public static final FieldParser DOUBLE = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			double min = Double.parseDouble(minQuery);
-			double max = Double.parseDouble(maxQuery);
-			return NumericRangeQuery.newDoubleRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.DOUBLE;
-		}
-	};
-
-}
--- a/src/luan/lib/queryparser/SaneQueryParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,261 +0,0 @@
-package luan.lib.queryparser;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.MatchAllDocsQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.Sort;
-import org.apache.lucene.search.SortField;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
-
-
-public class SaneQueryParser {
-
-	public static Query parseQuery(FieldParser fieldParser,String query) throws ParseException {
-		return new SaneQueryParser(fieldParser,query).parseQuery();
-	}
-
-	private static Pattern specialChar = Pattern.compile("[ \\t\\r\\n\":\\[\\]{}^+\\-(),?*\\\\]");
-
-	public static String literal(String s) {
-		return specialChar.matcher(s).replaceAll("\\\\$0");
-	}
-
-	public static Sort parseSort(FieldParser fieldParser,String sort) throws ParseException {
-		return new SaneQueryParser(fieldParser,sort).parseSort();
-	}
-
-
-	private static final String NOT_IN_RANGE = " \t\r\n\":[]{}^+()";
-	private static final String NOT_IN_TERM = NOT_IN_RANGE + "-";
-	private static final String NOT_IN_FIELD = NOT_IN_TERM + ",";
-	private final FieldParser fieldParser;
-	private final Parser parser;
-
-	private SaneQueryParser(FieldParser fieldParser,String query) {
-		this.fieldParser = fieldParser;
-		this.parser = new Parser(query);
-		parser.begin();
-	}
-
-	ParseException exception(String msg) {
-		parser.failure();
-		return new ParseException(parser,msg);
-	}
-
-	ParseException exception(Exception cause) {
-		parser.failure();
-		return new ParseException(parser,cause);
-	}
-
-	private Query parseQuery() throws ParseException {
-		Spaces();
-		BooleanQuery bq = new BooleanQuery();
-		while( !parser.endOfInput() ) {
-			bq.add( Term(null) );
-		}
-		BooleanClause[] clauses = bq.getClauses();
-		switch( clauses.length ) {
-		case 0:
-			return new MatchAllDocsQuery();
-		case 1:
-			{
-				BooleanClause bc = clauses[0];
-				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
-					return bc.getQuery();
-			}
-		default:
-			return bq;
-		}
-	}
-
-	private BooleanClause Term(String defaultField) throws ParseException {
-		BooleanClause.Occur occur;
-		if( parser.match('+') ) {
-			occur = BooleanClause.Occur.MUST;
-			Spaces();
-		} else if( parser.match('-') ) {
-			occur = BooleanClause.Occur.MUST_NOT;
-			Spaces();
-		} else {
-			occur = BooleanClause.Occur.SHOULD;
-		}
-		String field = QueryField();
-		if( field == null )
-			field = defaultField;
-		Query query = NestedTerm(field);
-		if( query == null )
-			query = RangeTerm(field);
-		if( query == null ) {
-			parser.begin();
-			String match = SimpleTerm(NOT_IN_TERM);
-			query = fieldParser.getQuery(this,field,match);
-			parser.success();
-		}
-		if( parser.match('^') ) {
-			Spaces();
-			int start = parser.begin();
-			try {
-				while( parser.anyOf("0123456789.") );
-				String match = parser.textFrom(start);
-				float boost = Float.parseFloat(match);
-				query.setBoost(boost);
-			} catch(NumberFormatException e) {
-				throw exception(e);
-			}
-			parser.success();
-			Spaces();
-		}
-		BooleanClause bc = new BooleanClause(query,occur);
-		return bc;
-	}
-
-	private Query NestedTerm(String field) throws ParseException {
-		parser.begin();
-		if( !parser.match('(') )
-			return parser.failure(null);
-		BooleanQuery bq = new BooleanQuery();
-		while( !parser.match(')') ) {
-			if( parser.endOfInput() )
-				throw exception("unclosed parentheses");
-			bq.add( Term(field) );
-		}
-		Spaces();
-		BooleanClause[] clauses = bq.getClauses();
-		switch( clauses.length ) {
-		case 0:
-			throw exception("empty parentheses");
-		case 1:
-			{
-				BooleanClause bc = clauses[0];
-				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
-					return parser.success(bc.getQuery());
-			}
-		default:
-			return parser.success(bq);
-		}
-	}
-
-	private Query RangeTerm(String field) throws ParseException {
-		parser.begin();
-		if( !parser.anyOf("[{") )
-			return parser.failure(null);
-		boolean includeMin = parser.lastChar() == '[';
-		Spaces();
-		String minQuery = SimpleTerm(NOT_IN_RANGE);
-		TO();
-		String maxQuery = SimpleTerm(NOT_IN_RANGE);
-		if( !parser.anyOf("]}") )
-			throw exception("unclosed range");
-		boolean includeMax = parser.lastChar() == ']';
-		Spaces();
-		Query query = fieldParser.getRangeQuery(this,field,minQuery,maxQuery,includeMin,includeMax);
-		return parser.success(query);
-	}
-
-	private void TO() throws ParseException {
-		parser.begin();
-		if( !(parser.match("TO") && Space()) )
-			throw exception("'TO' expected");
-		Spaces();
-		parser.success();
-	}
-
-	private String SimpleTerm(String exclude) throws ParseException {
-		parser.begin();
-		String match;
-		if( parser.match('"') ) {
-			int start = parser.currentIndex() - 1;
-			while( !parser.match('"') ) {
-				if( parser.endOfInput() )
-					throw exception("unclosed quotes");
-				parser.anyChar();
-				checkEscape();
-			}
-			match = parser.textFrom(start);
-			Spaces();
-		} else {
-			match = Unquoted(exclude);
-		}
-		if( match.length() == 0 )
-			throw exception("invalid input");
-		return parser.success(match);
-	}
-
-	private String QueryField() throws ParseException {
-		parser.begin();
-		String match = Field();
-		if( match==null || !parser.match(':') )
-			return parser.failure((String)null);
-		Spaces();
-		return parser.success(match);
-	}
-
-	private String Field() throws ParseException {
-		parser.begin();
-		String match = Unquoted(NOT_IN_FIELD);
-		if( match.length()==0 )
-			return parser.failure((String)null);
-		match = StringFieldParser.escape(this,match);
-		return parser.success(match);
-	}
-
-	private String Unquoted(String exclude) throws ParseException {
-		int start = parser.begin();
-		while( parser.noneOf(exclude) ) {
-			checkEscape();
-		}
-		String match = parser.textFrom(start);
-		Spaces();
-		return parser.success(match);
-	}
-
-	private void checkEscape() {
-		if( parser.lastChar() == '\\' )
-			parser.anyChar();
-	}
-
-	private void Spaces() {
-		while( Space() );
-	}
-
-	private boolean Space() {
-		return parser.anyOf(" \t\r\n");
-	}
-
-
-	// sort
-
-	private Sort parseSort() throws ParseException {
-		Spaces();
-		if( parser.endOfInput() )
-			return null;
-		List<SortField> list = new ArrayList<SortField>();
-		list.add( SortField() );
-		while( !parser.endOfInput() ) {
-			parser.begin();
-			if( !parser.match(',') )
-				throw exception("',' expected");
-			Spaces();
-			parser.success();
-			list.add( SortField() );
-		}
-		return new Sort(list.toArray(new SortField[0]));
-	}
-
-	private SortField SortField() throws ParseException {
-		parser.begin();
-		String field = Field();
-		if( field==null )
-			throw exception("invalid input");
-		boolean reverse = !parser.matchIgnoreCase("asc") && parser.matchIgnoreCase("desc");
-		Spaces();
-		SortField sf = fieldParser.getSortField(this,field,reverse);
-		return parser.success(sf);
-	}
-
-}
--- a/src/luan/lib/queryparser/StringFieldParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-package luan.lib.queryparser;
-
-import java.io.StringReader;
-import java.io.IOException;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.analysis.TokenStream;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.search.TermRangeQuery;
-import org.apache.lucene.search.PhraseQuery;
-import org.apache.lucene.search.WildcardQuery;
-import org.apache.lucene.search.PrefixQuery;
-import org.apache.lucene.search.SortField;
-import org.apache.lucene.index.Term;
-import luan.lib.parser.ParseException;
-
-
-public class StringFieldParser implements FieldParser {
-	public int slop = 0;
-	public final Analyzer analyzer;
-
-	public StringFieldParser(Analyzer analyzer) {
-		this.analyzer = analyzer;
-	}
-
-	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		String wildcard = wildcard(qp,query);
-		if( wildcard != null )
-			return new WildcardQuery(new Term(field,wildcard));
-		if( query.endsWith("*") && !query.endsWith("\\*") )
-			return new PrefixQuery(new Term(field,query.substring(0,query.length()-1)));
-		query = escape(qp,query);
-		PhraseQuery pq = new PhraseQuery();
-		try {
-			TokenStream ts = analyzer.tokenStream(field,new StringReader(query));
-			CharTermAttribute termAttr = ts.addAttribute(CharTermAttribute.class);
-			PositionIncrementAttribute posAttr = ts.addAttribute(PositionIncrementAttribute.class);
-			ts.reset();
-			int pos = -1;
-			while( ts.incrementToken() ) {
-				pos += posAttr.getPositionIncrement();
-				pq.add( new Term(field,termAttr.toString()), pos );
-			}
-			ts.end();
-			ts.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		Term[] terms = pq.getTerms();
-		if( terms.length==1 && pq.getPositions()[0]==0 )
-			return new TermQuery(terms[0]);
-		return pq;
-	}
-
-	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		minQuery = escape(qp,minQuery);
-		maxQuery = escape(qp,maxQuery);
-		return TermRangeQuery.newStringRange(field,minQuery,maxQuery,includeMin,includeMax);
-	}
-
-	static String escape(SaneQueryParser qp,String s) throws ParseException {
-		final char[] a = s.toCharArray();
-		int i, n;
-		if( a[0] == '"' ) {
-			if( a[a.length-1] != '"' )  throw new RuntimeException();
-			i = 1;
-			n = a.length - 1;
-		} else {
-			i = 0;
-			n = a.length;
-		}
-		StringBuilder sb = new StringBuilder();
-		for( ; i<n; i++ ) {
-			char c = a[i];
-			if( c == '\\' ) {
-				if( ++i == a.length )
-					throw qp.exception("ends with '\\'");
-				c = a[i];
-			}
-			sb.append(c);
-		}
-		return sb.toString();
-	}
-
-	private static String wildcard(SaneQueryParser qp,String s) throws ParseException {
-		final char[] a = s.toCharArray();
-		if( a[0] == '"' )
-			return null;
-		boolean hasWildcard = false;
-		StringBuilder sb = new StringBuilder();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			if( c=='?' || c=='*' && i<a.length-1 )
-				hasWildcard = true;
-			if( c == '\\' ) {
-				if( ++i == a.length )
-					throw qp.exception("ends with '\\'");
-				c = a[i];
-				if( c=='?' || c=='*' )
-					sb.append('\\');
-			}
-			sb.append(c);
-		}
-		return hasWildcard ? sb.toString() : null;
-	}
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
-		return new SortField( field, SortField.Type.STRING, reverse );
-	}
-
-}
--- a/src/luan/lib/queryparser/SynonymParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-package luan.lib.queryparser;
-
-import java.util.Map;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.SortField;
-import luan.lib.parser.ParseException;
-
-
-public class SynonymParser implements FieldParser {
-	private final FieldParser fp;
-	private final Map<String,String[]> synonymMap;
-
-	public SynonymParser(FieldParser fp,Map<String,String[]> synonymMap) {
-		this.fp = fp;
-		this.synonymMap = synonymMap;
-	}
-
-	protected String[] getSynonyms(String query) {
-		return synonymMap.get(query);
-	}
-
-	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		String[] synonyms = getSynonyms(query);
-		if( synonyms == null )
-			return fp.getQuery(qp,field,query);
-		BooleanQuery bq = new BooleanQuery();
-		bq.add( fp.getQuery(qp,field,query), BooleanClause.Occur.SHOULD );
-		for( String s : synonyms ) {
-			bq.add( fp.getQuery(qp,field,s), BooleanClause.Occur.SHOULD );
-		}
-		return bq;
-	}
-
-	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-	}
-
-	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
-		return fp.getSortField(qp,field,reverse);
-	}
-}
--- a/src/luan/lib/rpc/FixedLengthInputStream.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.InputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.EOFException;
-
-
-public class FixedLengthInputStream extends FilterInputStream {
-	private long left;
-
-	public FixedLengthInputStream(InputStream in,long len) {
-		super(in);
-		if( len < 0 )
-			throw new IllegalArgumentException("len can't be negative");
-		this.left = len;
-	}
-
-	public int read() throws IOException {
-		if( left == 0 )
-			return -1;
-		int n = in.read();
-		if( n == -1 )
-			throw new EOFException();
-		left--;
-		return n;
-	}
-
-	public int read(byte b[], int off, int len) throws IOException {
-		if( len == 0 )
-			return 0;
-		if( left == 0 )
-			return -1;
-		if( len > left )
-			len = (int)left;
-		int n = in.read(b, off, len);
-		if( n == -1 )
-			throw new EOFException();
-		left -= n;
-		return n;
-	}
-
-	public long skip(long n) throws IOException {
-		if( n > left )
-			n = left;
-		n = in.skip(n);
-		left -= n;
-		return n;
-	}
-
-	public int available() throws IOException {
-		int n = in.available();
-		if( n > left )
-			n = (int)left;
-		return n;
-	}
-
-    public void close() throws IOException {
-        while( left > 0 ) {
-			if( skip(left) == 0 )
-				throw new EOFException();
-		}
-    }
-
-	public void mark(int readlimit) {}
-
-	public void reset() throws IOException {
-		throw new IOException("not supported");
-	}
-
-	public boolean markSupported() {
-		return false;
-	}
-
-}
--- a/src/luan/lib/rpc/Rpc.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.IOException;
-
-
-// static utils
-public class Rpc {
-	private Rpc() {}  // never
-
-	public static final RpcResult OK = new RpcResult();
-
-	public static final RpcCall CLOSE = new RpcCall("close");
-	public static final RpcCall PING = new RpcCall("ping");
-	public static final String ECHO = "echo";
-
-	public static final RpcException COMMAND_NOT_FOUND = new RpcException("command_not_found");
-
-	public static boolean handle(RpcServer server,RpcCall call)
-		throws IOException
-	{
-		if( CLOSE.cmd.equals(call.cmd) ) {
-			server.close();
-			return true;
-		}
-		if( PING.cmd.equals(call.cmd) ) {
-			server.write(OK);
-			return true;
-		}
-		if( ECHO.equals(call.cmd) ) {
-			server.write(new RpcResult(call.args));
-			return true;
-		}
-		return false;
-	}
-
-}
--- a/src/luan/lib/rpc/RpcCall.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.InputStream;
-
-
-public final class RpcCall {
-	public final InputStream in;
-	public final long lenIn;
-	public final String cmd;
-	public final Object[] args;
-
-	public RpcCall(String cmd,Object... args) {
-		this(null,-1L,cmd,args);
-	}
-
-	public RpcCall(InputStream in,long lenIn,String cmd,Object... args) {
-		this.in = in;
-		this.lenIn = lenIn;
-		this.cmd = cmd;
-		this.args = args;
-	}
-}
--- a/src/luan/lib/rpc/RpcClient.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-package luan.lib.rpc;
-
-import java.net.Socket;
-import java.util.List;
-import java.util.ArrayList;
-
-
-public class RpcClient extends RpcCon {
-
-	public RpcClient(Socket socket)
-		throws RpcError
-	{
-		super(socket);
-	}
-
-	public void write(RpcCall call)
-		throws RpcError
-	{
-		List list = new ArrayList();
-		list.add(call.cmd);
-		for( Object arg : call.args ) {
-			list.add(arg);
-		}
-		write(call.in,call.lenIn,list);
-	}
-
-	public RpcResult read()
-		throws RpcError, RpcException
-	{
-		List list = readJson();
-		boolean ok = (Boolean)list.remove(0);
-		if( !ok ) {
-			String errorId = (String)list.remove(0);
-			Object[] args = list.toArray();
-			throw new RpcException(inBinary,lenBinary,errorId,args);
-		}
-		Object[] args = list.toArray();
-		return new RpcResult(inBinary,lenBinary,args);
-	}
-
-}
--- a/src/luan/lib/rpc/RpcCon.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.IOException;
-import java.io.EOFException;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import luan.lib.parser.ParseException;
-import luan.lib.json.JsonParser;
-import luan.lib.json.JsonToString;
-
-
-public class RpcCon {
-	final Socket socket;
-	final InputStream in;
-	final OutputStream out;
-	InputStream inBinary = null;
-	long lenBinary = -1;
-	boolean readSome = false;
-
-	RpcCon(Socket socket)
-		throws RpcError
-	{
-		try {
-			this.socket = socket;
-			this.in = socket.getInputStream();
-			this.out = socket.getOutputStream();
-		} catch(IOException e) {
-			close();
-			throw new RpcError(e);
-		}
-	}
-
-	public void close()
-		throws RpcError
-	{
-		try {
-			socket.close();
-		} catch(IOException e) {
-			throw new RpcError(e);
-		}
-	}
-
-	public boolean isClosed() {
-		return socket.isClosed();
-	}
-
-	void write(InputStream in,long lenIn,List list)
-		throws RpcError
-	{
-		if( in != null )
-			list.add(0,lenIn);
-		String json = JsonToString.toString(list);
-		byte[] aJson = json.getBytes(StandardCharsets.UTF_8);
-		int len = aJson.length;
-		byte[] a = new byte[4+len];
-        a[0] = (byte)(len >>> 24);
-        a[1] = (byte)(len >>> 16);
-        a[2] = (byte)(len >>>  8);
-        a[3] = (byte)(len >>>  0);
-		System.arraycopy(aJson,0,a,4,len);
-		try {
-			out.write(a);
-			if( in != null ) {
-				a = new byte[8192];
-				long total = 0;
-				int n;
-				while( (n=in.read(a)) != -1 ) {
-					out.write(a,0,n);
-					total += n;
-				}
-				if( total != lenIn ) {
-					close();
-					throw new RpcError("InputStream wrong length "+total+" when should be "+lenIn);
-				}
-			}
-		} catch(IOException e) {
-			close();
-			throw new RpcError(e);
-		}
-	}
-
-	List readJson()
-		throws RpcError
-	{
-		try {
-			if( inBinary != null ) {
-				inBinary.close();
-				inBinary = null;
-				lenBinary = -1;
-			}
-			readSome = false;
-			byte[] a = new byte[4];
-			readAll(a);
-			int len = 0;
-			for( byte b : a ) {
-				len <<= 8;
-				len |= b&0xFF;
-			}
-			a = new byte[len];
-			readAll(a);
-			String json = new String(a,StandardCharsets.UTF_8);
-			List list = (List)JsonParser.parse(json);
-			if( list.get(0) instanceof Long ) {
-				lenBinary = (Long)list.remove(0);
-				inBinary = new FixedLengthInputStream(in,lenBinary);
-			}
-			return list;
-		} catch(IOException e) {
-			close();
-			throw new RpcError(e);
-		} catch(ParseException e) {
-			close();
-			throw new RpcError(e);
-		}
-	}
-
-	private void readAll(final byte[] a) throws IOException {
-		int total = 0;
-		int n;
-		while( total < a.length ){
-			n = in.read( a, total, a.length-total );
-			if( n == -1 )
-				throw new EOFException();
-			readSome = true;
-			total += n;
-		}
-	}
-
-}
--- a/src/luan/lib/rpc/RpcError.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-package luan.lib.rpc;
-
-
-public class RpcError extends RuntimeException {
-
-	public RpcError(String msg) {
-		super(msg);
-	}
-
-	public RpcError(Exception e) {
-		super(e);
-	}
-
-}
--- a/src/luan/lib/rpc/RpcException.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.InputStream;
-
-
-public class RpcException extends Exception {
-	public final InputStream in;
-	public final long lenIn;
-	public final Object[] values;
-
-	public RpcException(String id,Object... values) {
-		this(null,-1,id,values);
-	}
-
-	public RpcException(InputStream in,long lenIn,String id,Object... values) {
-		super(id);
-		this.in = in;
-		this.lenIn = lenIn;
-		this.values = values;
-	}
-}
--- a/src/luan/lib/rpc/RpcResult.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.InputStream;
-
-
-public final class RpcResult {
-	public final InputStream in;
-	public final long lenIn;
-	public final Object[] returnValues;
-
-	public RpcResult(Object... returnValues) {
-		this(null,-1L,returnValues);
-	}
-
-	public RpcResult(InputStream in,long lenIn,Object... returnValues) {
-		this.in = in;
-		this.lenIn = lenIn;
-		this.returnValues = returnValues;
-	}
-}
--- a/src/luan/lib/rpc/RpcServer.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-package luan.lib.rpc;
-
-import java.io.EOFException;
-import java.net.Socket;
-import java.util.List;
-import java.util.ArrayList;
-
-
-public class RpcServer extends RpcCon {
-
-	public RpcServer(Socket socket)
-		throws RpcError
-	{
-		super(socket);
-	}
-
-	public RpcCall read()
-		throws RpcError
-	{
-		try {
-			List list = readJson();
-			String cmd = (String)list.remove(0);
-			Object[] args = list.toArray();
-			return new RpcCall(inBinary,lenBinary,cmd,args);
-		} catch(RpcError e) {
-			if( !readSome && e.getCause() instanceof EOFException )
-				return null;
-			throw e;
-		}
-	}
-
-	public void write(RpcResult result)
-		throws RpcError
-	{
-		List list = new ArrayList();
-		list.add(true);
-		for( Object val : result.returnValues ) {
-			list.add(val);
-		}
-		write(result.in,result.lenIn,list);
-	}
-
-	public void write(RpcException ex)
-		throws RpcError
-	{
-		List list = new ArrayList();
-		list.add(false);
-		list.add(ex.getMessage());
-		for( Object val : ex.values ) {
-			list.add(val);
-		}
-		write(ex.in,ex.lenIn,list);
-	}
-
-}
--- a/src/luan/lib/webserver/Connection.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,134 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.IOException;
-import java.net.Socket;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.parser.ParseException;
-
-
-final class Connection {
-	private static final Logger logger = LoggerFactory.getLogger(Connection.class);
-
-	static void handle(Server server,Socket socket) {
-		new Connection(server,socket).handle();
-	}
-
-	private final Server server;
-	private final Socket socket;
-
-	private Connection(Server server,Socket socket) {
-		this.server = server;
-		this.socket = socket;
-	}
-
-	private void handle() {
-		try {
-			Request request = new Request();
-			Response response;
-			try {
-				{
-					InputStream in = socket.getInputStream();
-					byte[] a = new byte[8192];
-					int endOfHeader;
-					int size = 0;
-					int left = a.length;
-					outer: while(true) {
-						int n = in.read(a,size,left);
-						if( n == -1 ) {
-							if( size == 0 ) {
-								socket.close();
-								return;
-							}
-							throw new IOException("unexpected end of input at "+size);
-						}
-						size += n;
-						for( int i=0; i<=size-4; i++ ) {
-							if( a[i]=='\r' && a[i+1]=='\n' && a[i+2]=='\r' && a[i+3]=='\n' ) {
-								endOfHeader = i + 4;
-								break outer;
-							}
-						}
-						left -= n;
-						if( left == 0 ) {
-							byte[] a2 = new byte[2*a.length];
-							System.arraycopy(a,0,a2,0,size);
-							a = a2;
-							left = a.length - size;
-						}
-					}
-					String rawHead = new String(a,0,endOfHeader);
-					//System.out.println(rawHead);
-					request.rawHead = rawHead;
-					RequestParser parser = new RequestParser(request);
-					parser.parseHead();
-		
-					String lenStr = (String)request.headers.get("content-length");
-					if( lenStr != null ) {
-						int len = Integer.parseInt(lenStr);
-						byte[] body = new byte[len];
-						size -= endOfHeader;
-						System.arraycopy(a,endOfHeader,body,0,size);
-						while( size < len ) {
-							int n = in.read(body,size,len-size);
-							if( n == -1 ) {
-								throw new IOException("unexpected end of input at "+size);
-							}
-							size += n;
-						}
-						request.body = body;
-						//System.out.println(new String(request.body));
-					}
-	
-					String contentType = (String)request.headers.get("content-type");
-					if( contentType != null ) {
-						contentType = contentType.toLowerCase();
-						if( "application/x-www-form-urlencoded".equals(contentType) ) {
-							parser.parseUrlencoded(null);
-						} else if( "application/x-www-form-urlencoded; charset=utf-8".equals(contentType) ) {
-							parser.parseUrlencoded("utf-8");
-						} else if( contentType.startsWith("multipart/form-data;") ) {
-							parser.parseMultipart();
-						} else if( contentType.equals("application/json; charset=utf-8") ) {
-							parser.parseJson();
-						} else {
-							logger.info("unknown request content-type: "+contentType);
-						}
-					}
-
-					String scheme = (String)request.headers.get("x-forwarded-proto");
-					if( scheme != null )
-						request.scheme = scheme;
-				}
-				response = server.handler.handle(request);
-			} catch(ParseException e) {
-				logger.warn("parse error\n"+request.rawHead.trim()+"\n",e);
-				response = Response.errorResponse(Status.BAD_REQUEST,e.toString());
-			}
-			response.headers.put("connection","close");
-			response.headers.put("content-length",Long.toString(response.body.length));
-			byte[] header = response.toHeaderString().getBytes();
-	
-			OutputStream out = socket.getOutputStream();
-			out.write(header);
-			copyAll(response.body.content,out);
-			out.close();
-			socket.close();
-		} catch(IOException e) {
-			logger.info("",e);
-		}
-	}
-
-	private static void copyAll(InputStream in,OutputStream out)
-		throws IOException
-	{
-		byte[] a = new byte[8192];
-		int n;
-		while( (n=in.read(a)) != -1 ) {
-			out.write(a,0,n);
-		}
-	}
-
-}
--- a/src/luan/lib/webserver/Handler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-package luan.lib.webserver;
-
-
-public interface Handler {
-	public Response handle(Request request);
-}
--- a/src/luan/lib/webserver/Request.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-package luan.lib.webserver;
-
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Collections;
-
-
-public class Request {
-	public volatile String rawHead;
-	public volatile String method;
-	public volatile String rawPath;
-	public volatile String originalPath;
-	public volatile String path;
-	public volatile String protocol;  // only HTTP/1.1 is accepted
-	public volatile String scheme;
-	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
-	public final Map<String,Object> parameters = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
-	public final Map<String,String> cookies = Collections.synchronizedMap(new LinkedHashMap<String,String>());
-	public volatile byte[] body;
-
-	public static final class MultipartFile {
-		public final String filename;
-		public final String contentType;
-		public final Object content;  // byte[] or String
-
-		public MultipartFile(String filename,String contentType,Object content) {
-			this.filename = filename;
-			this.contentType = contentType;
-			this.content = content;
-		}
-
-		public String toString() {
-			return "{filename="+filename+", content="+content+"}";
-		}
-	}
-}
--- a/src/luan/lib/webserver/RequestParser.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,287 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.util.List;
-import java.util.ArrayList;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
-
-
-final class RequestParser {
-	private static final Logger logger = LoggerFactory.getLogger(RequestParser.class);
-	private final Request request;
-	private Parser parser;
-
-	RequestParser(Request request) {
-		this.request = request;
-	}
-
-	void parseUrlencoded(String charset) throws ParseException, UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		this.parser = new Parser(Util.toString(request.body,charset));
-		parseQuery();
-		require( parser.endOfInput() );
-	}
-
-	void parseHead() throws ParseException {
-		this.parser = new Parser(request.rawHead);
-		parseRequestLine();
-		while( !parser.match("\r\n") ) {
-			parserHeaderField();
-		}
-		parseCookies();
-	}
-
-	private void parseRequestLine() throws ParseException {
-		parseMethod();
-		require( parser.match(' ') );
-		parseRawPath();
-		require( parser.match(' ') );
-		parseProtocol();
-		require( parser.match("\r\n") );
-	}
-
-	private void parseMethod() throws ParseException {
-		int start = parser.currentIndex();
-		if( !methodChar() )
-			throw new ParseException(parser,"no method");
-		while( methodChar() );
-		request.method = parser.textFrom(start);
-	}
-
-	private boolean methodChar() {
-		return parser.inCharRange('A','Z');
-	}
-
-	private void parseRawPath() throws ParseException {
-		int start = parser.currentIndex();
-		parsePath();
-		if( parser.match('?') )
-			parseQuery();
-		request.rawPath = parser.textFrom(start);
-	}
-
-	private void parsePath() throws ParseException {
-		int start = parser.currentIndex();
-		if( !parser.match('/') )
-			throw new ParseException(parser,"bad path");
-		while( parser.noneOf(" ?#") );
-		request.path = urlDecode( parser.textFrom(start) );
-		request.originalPath = request.path;
-	}
-
-	private void parseQuery() throws ParseException {
-		do {
-			int start = parser.currentIndex();
-			while( queryChar() );
-			String name = urlDecode( parser.textFrom(start) );
-			String value = null;
-			if( parser.match('=') ) {
-				start = parser.currentIndex();
-				while( queryChar() || parser.match('=') );
-				value = urlDecode( parser.textFrom(start) );
-			}
-			if( name.length() > 0 || value != null ) {
-				if( value==null )
-					value = "";
-				Util.add(request.parameters,name,value);
-			}
-		} while( parser.match('&') );
-	}
-
-	private boolean queryChar() {
-		return parser.noneOf("=&# \t\n\f\r\u000b");
-	}
-
-	private void parseProtocol() throws ParseException {
-		int start = parser.currentIndex();
-		if( !(
-			parser.match("HTTP/")
-			&& parser.inCharRange('0','9')
-			&& parser.match('.')
-			&& parser.inCharRange('0','9')
-		) )
-			throw new ParseException(parser,"bad protocol");
-		request.protocol = parser.textFrom(start);
-		request.scheme = "http";
-	}
-
-
-	private void parserHeaderField() throws ParseException {
-		String name = parseName();
-		require( parser.match(':') );
-		while( parser.anyOf(" \t") );
-		String value = parseValue();
-		while( parser.anyOf(" \t") );
-		require( parser.match("\r\n") );
-		Util.add(request.headers,name,value);
-	}
-
-	private String parseName() throws ParseException {
-		int start = parser.currentIndex();
-		require( tokenChar() );
-		while( tokenChar() );
-		return parser.textFrom(start).toLowerCase();
-	}
-
-	private String parseValue() throws ParseException {
-		int start = parser.currentIndex();
-		while( !testEndOfValue() )
-			require( parser.anyChar() );
-		return parser.textFrom(start);
-	}
-
-	private boolean testEndOfValue() {
-		parser.begin();
-		while( parser.anyOf(" \t") );
-		boolean b = parser.endOfInput() || parser.anyOf("\r\n");
-		parser.failure();  // rollback
-		return b;
-	}
-
-	private void require(boolean b) throws ParseException {
-		if( !b )
-			throw new ParseException(parser,"failed");
-	}
-
-	boolean tokenChar() {
-		if( parser.endOfInput() )
-			return false;
-		char c = parser.currentChar();
-		if( 32 <= c && c <= 126 && "()<>@,;:\\\"/[]?={} \t\r\n".indexOf(c) == -1 ) {
-			parser.anyChar();
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-
-	private void parseCookies() throws ParseException {
-		String text = (String)request.headers.get("cookie");
-		if( text == null )
-			return;
-		this.parser = new Parser(text);
-		while(true) {
-			int start = parser.currentIndex();
-			while( parser.noneOf("=;") );
-			String name = urlDecode( parser.textFrom(start) );
-			if( parser.match('=') ) {
-				start = parser.currentIndex();
-				while( parser.noneOf(";") );
-				String value = parser.textFrom(start);
-				int len = value.length();
-				if( value.charAt(0)=='"' && value.charAt(len-1)=='"' )
-					value = value.substring(1,len-1);
-				value = urlDecode(value);
-				request.cookies.put(name,value);
-			}
-			if( parser.endOfInput() )
-				return;
-			require( parser.match(';') );
-			parser.match(' ');  // optional for bad browsers
-		}
-	}
-
-
-	private static final String contentTypeStart = "multipart/form-data; boundary=";
-
-	void parseMultipart() throws ParseException, UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		String contentType = (String)request.headers.get("content-type");
-		if( !contentType.startsWith(contentTypeStart) )
-			throw new RuntimeException(contentType);
-		String boundary = "--"+contentType.substring(contentTypeStart.length());
-		this.parser = new Parser(Util.toString(request.body,null));
-//System.out.println(this.parser.text);
-		require( parser.match(boundary) );
-		boundary = "\r\n" + boundary;
-		while( !parser.match("--\r\n") ) {
-			require( parser.match("\r\n") );
-			require( parser.match("Content-Disposition: form-data; name=") );
-			String name = quotedString();
-			String filename = null;
-			boolean isBinary = false;
-			if( parser.match("; filename=") ) {
-				filename = quotedString();
-				require( parser.match("\r\n") );
-				require( parser.match("Content-Type: ") );
-				int start = parser.currentIndex();
-				if( parser.match("application/") ) {
-					isBinary = true;
-				} else if( parser.match("image/") ) {
-					isBinary = true;
-				} else if( parser.match("text/") ) {
-					isBinary = false;
-				} else
-					throw new ParseException(parser,"bad file content-type");
-				while( parser.inCharRange('a','z') || parser.anyOf("-.") );
-				contentType = parser.textFrom(start);
-			}
-			require( parser.match("\r\n") );
-			require( parser.match("\r\n") );
-			int start = parser.currentIndex();
-			while( !parser.test(boundary) ) {
-				require( parser.anyChar() );
-			}
-			String value = parser.textFrom(start);
-			if( filename == null ) {
-				Util.add(request.parameters,name,value);
-			} else {
-				Object content = isBinary ? Util.toBytes(value) : value;
-				Request.MultipartFile mf = new Request.MultipartFile(filename,contentType,content);
-				Util.add(request.parameters,name,mf);
-			}
-			require( parser.match(boundary) );
-		}
-	}
-
-	private String quotedString() throws ParseException {
-		StringBuilder sb = new StringBuilder();
-		require( parser.match('"') );
-		while( !parser.match('"') ) {
-			if( parser.match("\\\"") ) {
-				sb.append('"');
-			} else {
-				require( parser.anyChar() );
-				sb.append( parser.lastChar() );
-			}
-		}
-		return sb.toString();
-	}
-
-	private String urlDecode(String s) throws ParseException {
-		try {
-			return URLDecoder.decode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			parser.rollback();
-			throw new ParseException(parser,e);
-		} catch(IllegalArgumentException e) {
-			parser.rollback();
-			throw new ParseException(parser,e);
-		}
-	}
-
-	// improve later
-	void parseJson() throws UnsupportedEncodingException {
-		if( request.body == null ) {
-			logger.warn("body is null\n"+request.rawHead);
-			return;
-		}
-		String contentType = (String)request.headers.get("content-type");
-		if( !contentType.equals("application/json; charset=utf-8") )
-			throw new RuntimeException(contentType);
-		String value = new String(request.body,"utf-8");
-		Util.add(request.parameters,"json",value);
-	}
-
-}
--- a/src/luan/lib/webserver/Response.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Collections;
-import java.util.List;
-
-
-public class Response {
-	public final String protocol = "HTTP/1.1";
-	public volatile Status status = Status.OK;
-	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
-	{
-		headers.put("server","Luan");
-	}
-	private static final Body empty = new Body(0,new InputStream(){
-		public int read() { return -1; }
-	});
-	public volatile Body body = empty;
-
-	public static class Body {
-		public final long length;
-		public final InputStream content;
-	
-		public Body(long length,InputStream content) {
-			this.length = length;
-			this.content = content;
-		}
-	}
-
-
-	public void addHeader(String name,String value) {
-		Util.add(headers,name,value);
-	}
-
-	public void setCookie(String name,String value,Map<String,String> attributes) {
-		StringBuilder buf = new StringBuilder();
-		buf.append( Util.urlEncode(name) );
-		buf.append( '=' );
-		buf.append( Util.urlEncode(value) );
-		for( Map.Entry<String,String> entry : attributes.entrySet() ) {
-			buf.append( "; " );
-			buf.append( entry.getKey() );
-			buf.append( '=' );
-			buf.append( entry.getValue() );
-		}
-		addHeader( "Set-Cookie", buf.toString() );
-	}
-
-
-	public String toHeaderString() {
-		StringBuilder sb = new StringBuilder();
-		sb.append( protocol )
-			.append( ' ' ).append( status.code )
-			.append( ' ' ).append( status.reason )
-			.append( "\r\n" )
-		;
-		for( Map.Entry<String,Object> entry : headers.entrySet() ) {
-			String name = entry.getKey();
-			Object value = entry.getValue();
-			if( value instanceof List ) {
-				for( Object v : (List)value ) {
-					sb.append( name ).append( ": " ).append( v ).append( "\r\n" );
-				}
-			} else {
-				sb.append( name ).append( ": " ).append( value ).append( "\r\n" );
-			}
-		}
-		sb.append( "\r\n" );
-		return sb.toString();
-	}
-
-
-	public static Response errorResponse(Status status,String text) {
-		Response response = new Response();
-		response.status = status;
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
-		writer.write( text );
-		writer.close();
-		return response;
-	}
-}
--- a/src/luan/lib/webserver/ResponseOutputStream.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.ByteArrayOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-
-
-// plenty of room for improvement
-public class ResponseOutputStream extends ByteArrayOutputStream {
-	private final Response response;
-
-	public ResponseOutputStream(Response response) {
-		if(response==null) throw new NullPointerException();
-		this.response = response;
-	}
-
-	@Override public void close() throws IOException {
-		super.close();
-		int size = size();
-		response.body = new Response.Body( size, new ByteArrayInputStream(buf,0,size) );
-	}
-}
--- a/src/luan/lib/webserver/Server.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.IOException;
-import java.net.Socket;
-import java.net.ServerSocket;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-
-
-public class Server {
-	private static final Logger logger = LoggerFactory.getLogger(Server.class);
-
-	public final int port;
-	public final Handler handler;
-	public static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor)Executors.newCachedThreadPool();
-
-	public Server(int port,Handler handler) {
-		this.port = port;
-		this.handler = handler;
-	}
-
-	protected ServerSocket newServerSocket() throws IOException {
-		return new ServerSocket(port);
-	}
-
-	public synchronized void start() throws IOException {
-		final ServerSocket ss = newServerSocket();
-		threadPool.execute(new Runnable(){public void run() {
-			try {
-				while(!threadPool.isShutdown()) {
-					final Socket socket = ss.accept();
-					threadPool.execute(new Runnable(){public void run() {
-						Connection.handle(Server.this,socket);
-					}});
-				}
-			} catch(IOException e) {
-				logger.error("",e);
-			}
-		}});
-		logger.info("started server on port "+port);
-	}
-
-	public synchronized boolean stop(long timeoutSeconds) {
-		try {
-			threadPool.shutdownNow();
-			boolean stopped = threadPool.awaitTermination(timeoutSeconds,TimeUnit.SECONDS);
-			if(stopped)
-				logger.info("stopped server on port "+port);
-			else
-				logger.warn("couldn't stop server on port "+port);
-			return stopped;
-		} catch(InterruptedException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static class ForAddress extends Server {
-		private final InetAddress addr;
-
-		public ForAddress(InetAddress addr,int port,Handler handler) {
-			super(port,handler);
-			this.addr = addr;
-		}
-
-		public ForAddress(String addrName,int port,Handler handler) throws UnknownHostException {
-			this(InetAddress.getByName(addrName),port,handler);
-		}
-
-		protected ServerSocket newServerSocket() throws IOException {
-			return new ServerSocket(port,0,addr);
-		}
-	}
-}
--- a/src/luan/lib/webserver/Status.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-package luan.lib.webserver;
-
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-
-
-public class Status {
-	private static final Logger logger = LoggerFactory.getLogger(Status.class);
-
-	public final int code;
-	public final String reason;
-
-	public Status(int code,String reason) {
-		this.code = code;
-		this.reason = reason;
-	}
-
-	private static final Map<Integer,Status> map = new HashMap<Integer,Status>();
-
-	protected static Status newStatus(int code,String reason) {
-		Status status = new Status(code,reason);
-		map.put(code,status);
-		return status;
-	}
-
-	public static Status getStatus(int code) {
-		Status status = map.get(code);
-		if( status == null ) {
-			logger.warn("missing status "+code);
-			status = new Status(code,"");
-		}
-		return status;
-	}
-
-	public static final Status OK = newStatus(200,"OK");
-	public static final Status MOVED_PERMANENTLY = newStatus(301,"Moved Permanently");
-	public static final Status FOUND = newStatus(302,"Found");
-	public static final Status BAD_REQUEST = newStatus(400,"Bad Request");
-	public static final Status NOT_FOUND = newStatus(404,"Not Found");
-	public static final Status INTERNAL_SERVER_ERROR = newStatus(500,"Internal Server Error");
-}
--- a/src/luan/lib/webserver/Util.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-package luan.lib.webserver;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Map;
-import java.util.List;
-import java.util.ArrayList;
-
-
-final class Util {
-
-	static String urlEncode(String s) {
-		try {
-			return URLEncoder.encode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	static void add(Map<String,Object> map,String name,Object value) {
-		Object current = map.get(name);
-		if( current == null ) {
-			map.put(name,value);
-		} else if( current instanceof List ) {
-			List list = (List)current;
-			list.add(value);
-		} else {
-			List list = new ArrayList();
-			list.add(current);
-			list.add(value);
-			map.put(name,list);
-		}
-	}
-
-	static String toString(byte[] a,String charset) throws UnsupportedEncodingException {
-		if( charset != null )
-			return new String(a,charset);
-		char[] ac = new char[a.length];
-		for( int i=0; i<a.length; i++ ) {
-			ac[i] = (char)a[i];
-		}
-		return new String(ac);
-	}
-
-	static byte[] toBytes(String s) {
-		char[] ac = s.toCharArray();
-		byte[] a = new byte[ac.length];
-		for( int i=0; i<ac.length; i++ ) {
-			a[i] = (byte)ac[i];
-		}
-		return a;
-	}
-
-	private Util() {}  // never
-}
--- a/src/luan/lib/webserver/examples/Cookies.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-package luan.lib.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-
-
-public final class Cookies implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		String name = (String)request.parameters.get("name");
-		if( name != null ) {
-			Map<String,String> attributes = new HashMap<String,String>();
-			String value = (String)request.parameters.get("value");
-			if( value != null ) {
-				response.setCookie(name,value,attributes);
-			} else {
-				attributes.put("Max-Age","0");
-				response.setCookie(name,"delete",attributes);
-			}
-		}
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,String> entry : request.cookies.entrySet() ) {
-				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
--- a/src/luan/lib/webserver/examples/Example.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-package luan.lib.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import java.util.HashMap;
-import org.apache.log4j.EnhancedPatternLayout;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Logger;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-import luan.lib.webserver.Server;
-import luan.lib.webserver.handlers.MapHandler;
-import luan.lib.webserver.handlers.SafeHandler;
-import luan.lib.webserver.handlers.LogHandler;
-import luan.lib.webserver.handlers.FileHandler;
-import luan.lib.webserver.handlers.DirHandler;
-import luan.lib.webserver.handlers.ListHandler;
-import luan.lib.webserver.handlers.ContentTypeHandler;
-
-
-public class Example implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			writer.write("Hello World\n");
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException("shouldn't happen",e);
-		}
-		return response;
-	}
-
-	public static void simple() throws IOException {
-		Handler handler = new Example();
-		new Server(8080,handler).start();
-	}
-
-	public static void fancy() throws IOException {
-		Map<String,Handler> map = new HashMap<String,Handler>();
-		map.put( "/hello", new Example() );
-		map.put( "/headers", new Headers() );
-		map.put( "/params", new Params() );
-		map.put( "/cookies", new Cookies() );
-		Handler mapHandler = new MapHandler(map);
-		FileHandler fileHandler = new FileHandler();
-		Handler dirHandler = new DirHandler(fileHandler);
-		Handler handler = new ListHandler( mapHandler, fileHandler, dirHandler );
-		handler = new ContentTypeHandler(handler);
-		handler = new SafeHandler(handler);
-		handler = new LogHandler(handler);
-		new Server(8080,handler).start();
-	}
-
-	public static void initLogging() {
-//		Logger.getRootLogger().setLevel(Level.INFO);
-		EnhancedPatternLayout layout = new EnhancedPatternLayout("%d{HH:mm:ss} %-5p %c - %m%n");
-		ConsoleAppender appender = new ConsoleAppender(layout,"System.err");
-		Logger.getRootLogger().addAppender(appender);
-	}
-
-	public static void main(String[] args) throws Exception {
-		initLogging();
-		fancy();
-	}
-}
--- a/src/luan/lib/webserver/examples/Headers.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-package luan.lib.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-
-
-public final class Headers implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,Object> entry : request.headers.entrySet() ) {
-				writer.write(entry.getKey()+": "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
--- a/src/luan/lib/webserver/examples/Params.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-package luan.lib.webserver.examples;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.util.Map;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-
-
-public final class Params implements Handler {
-
-	public Response handle(Request request) {
-		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
-		try {
-			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-			for( Map.Entry<String,Object> entry : request.parameters.entrySet() ) {
-				writer.write(entry.getKey()+" = "+entry.getValue()+"\n");
-			}
-			writer.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		return response;
-	}
-
-}
--- a/src/luan/lib/webserver/examples/post.html	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<!doctype html>
-<html>
-	<body>
-		<form action=/params method=post>
-			<p>a <input name=a></p>
-			<p>b <input name=b></p>
-			<p><input type=submit></p>
-		</form>
-	</body>
-</html>
--- a/src/luan/lib/webserver/examples/post_multipart.html	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<!doctype html>
-<html>
-	<body>
-		<form action=/params method=post enctype="multipart/form-data">
-			<p>a <input name=a></p>
-			<p>b <input name=b></p>
-			<p><input type=file name=file></p>
-			<p><input type=submit></p>
-		</form>
-	</body>
-</html>
--- a/src/luan/lib/webserver/handlers/ContentTypeHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public class ContentTypeHandler implements Handler {
-	private final Handler handler;
-
-	// maps extension to content-type
-	// key must be lower case
-	public final Map<String,String> map = new HashMap<String,String>();
-
-	// set to null for none
-	public String contentTypeForNoExtension;
-
-	public ContentTypeHandler(Handler handler) {
-		this(handler,"utf-8");
-	}
-
-	public ContentTypeHandler(Handler handler,String charset) {
-		this.handler = handler;
-		String attrs = charset== null ? "" : "; charset="+charset;
-		String htmlType = "text/html" + attrs;
-		String textType = "text/plain" + attrs;
-		contentTypeForNoExtension = htmlType;
-		map.put( "html", htmlType );
-		map.put( "txt", textType );
-		map.put( "css", "text/css" );
-		map.put( "mp4", "video/mp4" );
-		// add more as need
-	}
-
-	public Response handle(Request request) {
-		Response response = handler.handle(request);
-		if( response!=null && !response.headers.containsKey("content-type") ) {
-			String path = request.path;
-			int iSlash = path.lastIndexOf('/');
-			int iDot = path.lastIndexOf('.');
-			String type;
-			if( iDot < iSlash ) {  // no extension
-				type = contentTypeForNoExtension;
-			} else {  // extension
-				String extension = path.substring(iDot+1);
-				type = map.get( extension.toLowerCase() );
-			}
-			if( type != null )
-				response.headers.put("content-type",type);
-		}
-		return response;
-	}
-}
--- a/src/luan/lib/webserver/handlers/DirHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.Date;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-
-
-public final class DirHandler implements Handler {
-	private final FileHandler fileHandler;
-
-	public DirHandler(FileHandler fileHandler) {
-		this.fileHandler = fileHandler;
-	}
-
-	private static final Comparator<File> sorter = new Comparator<File>() {
-		public int compare(File f1, File f2) {
-			return f1.getName().compareTo(f2.getName());
-		}
-	};
-
-	public Response handle(Request request) {
-		try {
-			File file = fileHandler.file(request);
-			if( request.path.endsWith("/") && file.isDirectory() ) {
-				DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
-				Response response = new Response();
-				response.headers.put( "content-type", "text/html; charset=utf-8" );
-				Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
-				writer.write( "<!doctype html><html>" );
-				writer.write( "<head><style>td{padding: 2px 8px}</style></head>" );
-				writer.write( "<body>" );
-				writer.write( "<h1>Directory: "+request.path+"</h1>" );
-				writer.write( "<table border=0>" );
-				File[] a = file.listFiles();
-				Arrays.sort(a,sorter);
-				for( File child : a ) {
-					String name = child.getName();
-					if( child.isDirectory() )
-						name += '/';
-					writer.write( "<tr>" );
-					writer.write( "<td><a href='"+name+"'>"+name+"</a></td>" );
-					writer.write( "<td>"+child.length()+" bytes</td>" );
-					writer.write( "<td>"+fmt.format(new Date(child.lastModified()))+"</td>" );
-					writer.write( "</tr>" );
-				}
-				writer.write( "</table>" );
-				writer.write( "</body>" );
-				writer.write( "</html>" );
-				writer.close();
-				return response;
-			}
-			return null;
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-}
--- a/src/luan/lib/webserver/handlers/DomainHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.lang.ref.Reference;
-//import java.lang.ref.WeakReference;
-import java.lang.ref.SoftReference;
-import java.lang.ref.ReferenceQueue;
-import java.util.Map;
-import java.util.HashMap;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public final class DomainHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger(DomainHandler.class);
-
-	public interface Factory {
-		public Handler newHandler(String domain);
-	}
-
-	private static void close(Handler handler) {
-		if( handler instanceof Closeable ) {
-			try {
-				((Closeable)handler).close();
-			} catch(IOException e) {
-				logger.error(handler.toString(),e);
-			}
-		}
-	}
-
-	private final Map<String,Reference<Handler>> map = new HashMap<String,Reference<Handler>>();
-
-	private final Factory factory;
-
-	public DomainHandler(Factory factory) {
-		this.factory = factory;
-	}
-
-	public Response handle(Request request) {
-		String host = (String)request.headers.get("host");
-		if( host == null )
-			return null;
-		int i = host.indexOf(':');
-		String domain = i == -1 ? host : host.substring(0,i);
-		Handler handler = getHandler(domain);
-		return handler==null ? null : handler.handle(request);
-	}
-
-	public Handler getHandler(String domain) {
-		domain = domain.toLowerCase();
-		synchronized(map) {
-			Reference<Handler> ref = map.get(domain);
-			Handler handler = ref==null ? null : ref.get();
-			if( handler == null ) {
-				//if(ref!=null) logger.info("gc "+domain);
-				handler = factory.newHandler(domain);
-				if( handler == null )
-					return null;
-				map.put(domain,new SoftReference<Handler>(handler));
-			}
-			return handler;
-		}
-	}
-
-	public void removeHandler(String domain) {
-		logger.info("removeHandler "+domain);
-		domain = domain.toLowerCase();
-		synchronized(map) {
-			Reference<Handler> ref = map.remove(domain);
-			Handler handler = ref==null ? null : ref.get();
-			if( handler != null ) {
-				close(handler);
-			}
-		}
-	}
-
-}
--- a/src/luan/lib/webserver/handlers/FileHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-
-
-public class FileHandler implements Handler {
-	final File dir;
-
-	public FileHandler() {
-		this(".");
-	}
-
-	public FileHandler(String pathname) {
-		this(new File(pathname));
-	}
-
-	public FileHandler(File dir) {
-		if( !dir.isDirectory() )
-			throw new RuntimeException("must be a directory");
-		this.dir = dir;
-	}
-
-	File file(Request request) {
-		return new File(dir,request.path);
-	}
-
-	public Response handle(Request request) {
-		try {
-			File file = file(request);
-			if( file.isFile() ) {
-				Response response = new Response();
-				response.body = new Response.Body( file.length(), new FileInputStream(file) );
-				return response;
-			}
-			return null;
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-}
--- a/src/luan/lib/webserver/handlers/IndexHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public final class IndexHandler implements Handler {
-	private final Handler handler;
-	private final String indexName;
-
-	public IndexHandler(Handler handler) {
-		this(handler,"index.html");
-	}
-
-	public IndexHandler(Handler handler,String indexName) {
-		this.handler = handler;
-		this.indexName = indexName;
-	}
-
-	public Response handle(Request request) {
-		if( request.path.endsWith("/") ) {
-			String path = request.path;
-			try {
-				request.path += indexName;
-				return handler.handle(request);
-			} finally {
-				request.path = path;
-			}
-		} else
-			return handler.handle(request);
-	}
-}
--- a/src/luan/lib/webserver/handlers/ListHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public final class ListHandler implements Handler {
-	private final Handler[] handlers;
-
-	public ListHandler(Handler... handlers) {
-		this.handlers = handlers;
-	}
-
-	public Response handle(Request request) {
-		for( Handler handler : handlers ) {
-			Response response = handler.handle(request);
-			if( response != null )
-				return response;
-		}
-		return null;
-	}
-}
--- a/src/luan/lib/webserver/handlers/LogHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public final class LogHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger("HTTP");
-
-	private final Handler handler;
-
-	public LogHandler(Handler handler) {
-		this.handler = handler;
-	}
-
-	public Response handle(Request request) {
-		Response response = handler.handle(request);
-		logger.info( request.method + " " + request.path + " " + response.status.code + " " + response.body.length );
-		return response;
-	}
-}
--- a/src/luan/lib/webserver/handlers/MapHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.util.Map;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-
-
-public final class MapHandler implements Handler {
-	private final Map<String,Handler> map;
-
-	public MapHandler(Map<String,Handler> map) {
-		this.map = map;
-	}
-
-	public Response handle(Request request) {
-		Handler handler = map.get(request.path);
-		return handler==null ? null : handler.handle(request);
-	}
-}
--- a/src/luan/lib/webserver/handlers/SafeHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-package luan.lib.webserver.handlers;
-
-import java.io.Writer;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.IOException;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.ResponseOutputStream;
-import luan.lib.webserver.Status;
-
-
-public final class SafeHandler implements Handler {
-	private static final Logger logger = LoggerFactory.getLogger(SafeHandler.class);
-
-	private final Handler handler;
-
-	public SafeHandler(Handler handler) {
-		this.handler = handler;
-	}
-
-	public Response handle(Request request) {
-		try {
-			Response response = handler.handle(request);
-			if( response != null )
-				return response;
-		} catch(RuntimeException e) {
-			logger.error("",e);
-			Response response = new Response();
-			response.status = Status.INTERNAL_SERVER_ERROR;
-			response.headers.put( "content-type", "text/plain; charset=utf-8" );
-			PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
-			writer.write( "Internel Server Error\n\n" );
-			e.printStackTrace(writer);
-			writer.close();
-			return response;
-		}
-		return Response.errorResponse( Status.NOT_FOUND, request.path+" not found\n" );
-	}
-
-}
--- a/src/luan/modules/Html.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/Html.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -2,7 +2,7 @@
 local HtmlLuan = require "java:luan.modules.HtmlLuan"
 local HtmlParser = require "java:luan.modules.parsers.Html"
 local URLEncoder = require "java:java.net.URLEncoder"
-local JsonToString = require "java:luan.lib.json.JsonToString"
+local JsonToString = require "java:goodjava.json.JsonToString"
 local Luan = require "luan:Luan.luan"
 local error = Luan.error
 local ipairs = Luan.ipairs or error()
--- a/src/luan/modules/Parsers.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/Parsers.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -17,8 +17,8 @@
 local Table = require "luan:Table.luan"
 local java_to_table_deep = Table.java_to_table_deep or error()
 local LuanJava = require "java:luan.Luan"
-local JsonParser = require "java:luan.lib.json.JsonParser"
-local JsonToString = require "java:luan.lib.json.JsonToString"
+local JsonParser = require "java:goodjava.json.JsonParser"
+local JsonToString = require "java:goodjava.json.JsonToString"
 
 -- converts json string to luan object
 function Parsers.json_parse(s)
--- a/src/luan/modules/Rpc.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/Rpc.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -3,12 +3,12 @@
 local ServerSocket = require "java:java.net.ServerSocket"
 local SSLSocketFactory = require "java:javax.net.ssl.SSLSocketFactory"
 local SSLServerSocketFactory = require "java:javax.net.ssl.SSLServerSocketFactory"
-local RpcClient = require "java:luan.lib.rpc.RpcClient"
-local RpcServer = require "java:luan.lib.rpc.RpcServer"
-local RpcCall = require "java:luan.lib.rpc.RpcCall"
-local RpcResult = require "java:luan.lib.rpc.RpcResult"
-local RpcException = require "java:luan.lib.rpc.RpcException"
-local JavaRpc = require "java:luan.lib.rpc.Rpc"
+local RpcClient = require "java:goodjava.rpc.RpcClient"
+local RpcServer = require "java:goodjava.rpc.RpcServer"
+local RpcCall = require "java:goodjava.rpc.RpcCall"
+local RpcResult = require "java:goodjava.rpc.RpcResult"
+local RpcException = require "java:goodjava.rpc.RpcException"
+local JavaRpc = require "java:goodjava.rpc.Rpc"
 local LuanJava = require "java:luan.Luan"
 local JavaUtils = require "java:luan.modules.Utils"
 local IoLuan = require "java:luan.modules.IoLuan"
--- a/src/luan/modules/ThreadLuan.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/ThreadLuan.java	Tue Sep 17 01:35:01 2019 -0400
@@ -24,8 +24,8 @@
 import luan.LuanException;
 import luan.LuanCloner;
 import luan.LuanCloneable;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 
 
 public final class ThreadLuan {
--- a/src/luan/modules/http/Http.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/http/Http.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -16,10 +16,10 @@
 local trim = String.trim or error()
 local Boot = require "luan:Boot.luan"
 local LuanJava = require "java:luan.Luan"
-local Request = require "java:luan.lib.webserver.Request"
-local Response = require "java:luan.lib.webserver.Response"
-local ResponseOutputStream = require "java:luan.lib.webserver.ResponseOutputStream"
-local Status = require "java:luan.lib.webserver.Status"
+local Request = require "java:goodjava.webserver.Request"
+local Response = require "java:goodjava.webserver.Response"
+local ResponseOutputStream = require "java:goodjava.webserver.ResponseOutputStream"
+local Status = require "java:goodjava.webserver.Status"
 local OutputStreamWriter = require "java:java.io.OutputStreamWriter"
 local HashMap = require "java:java.util.HashMap"
 local Logging = require "luan:logging/Logging.luan"
--- a/src/luan/modules/http/LuanDomainHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/http/LuanDomainHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -1,9 +1,9 @@
 package luan.modules.http;
 
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.handlers.DomainHandler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.Handler;
+import goodjava.webserver.handlers.DomainHandler;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanCloner;
--- a/src/luan/modules/http/LuanHandler.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/http/LuanHandler.java	Tue Sep 17 01:35:01 2019 -0400
@@ -15,14 +15,14 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.Status;
-import luan.lib.webserver.Server;
-import luan.lib.webserver.Handler;
-import luan.lib.webserver.ResponseOutputStream;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.Status;
+import goodjava.webserver.Server;
+import goodjava.webserver.Handler;
+import goodjava.webserver.ResponseOutputStream;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanFunction;
--- a/src/luan/modules/http/NotFound.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/http/NotFound.java	Tue Sep 17 01:35:01 2019 -0400
@@ -1,8 +1,8 @@
 package luan.modules.http;
 
-import luan.lib.webserver.Request;
-import luan.lib.webserver.Response;
-import luan.lib.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.Handler;
 
 
 public class NotFound implements Handler {
--- a/src/luan/modules/http/Server.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/http/Server.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -15,14 +15,14 @@
 local logger = Logging.logger "http/Server"
 
 require "java"
-local JavaServer = require "java:luan.lib.webserver.Server"
-local FileHandler = require "java:luan.lib.webserver.handlers.FileHandler"
-local DirHandler = require "java:luan.lib.webserver.handlers.DirHandler"
-local IndexHandler = require "java:luan.lib.webserver.handlers.IndexHandler"
-local ContentTypeHandler = require "java:luan.lib.webserver.handlers.ContentTypeHandler"
-local SafeHandler = require "java:luan.lib.webserver.handlers.SafeHandler"
-local LogHandler = require "java:luan.lib.webserver.handlers.LogHandler"
-local ListHandler = require "java:luan.lib.webserver.handlers.ListHandler"
+local JavaServer = require "java:goodjava.webserver.Server"
+local FileHandler = require "java:goodjava.webserver.handlers.FileHandler"
+local DirHandler = require "java:goodjava.webserver.handlers.DirHandler"
+local IndexHandler = require "java:goodjava.webserver.handlers.IndexHandler"
+local ContentTypeHandler = require "java:goodjava.webserver.handlers.ContentTypeHandler"
+local SafeHandler = require "java:goodjava.webserver.handlers.SafeHandler"
+local LogHandler = require "java:goodjava.webserver.handlers.LogHandler"
+local ListHandler = require "java:goodjava.webserver.handlers.ListHandler"
 local LuanHandler = require "java:luan.modules.http.LuanHandler"
 local System = require "java:java.lang.System"
 local NotFound = require "java:luan.modules.http.NotFound"
--- a/src/luan/modules/logging/LuanLogger.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/logging/LuanLogger.java	Tue Sep 17 01:35:01 2019 -0400
@@ -1,7 +1,7 @@
 package luan.modules.logging;
 
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 import luan.Luan;
 import luan.LuanException;
 
--- a/src/luan/modules/lucene/Lucene.luan	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/lucene/Lucene.luan	Tue Sep 17 01:35:01 2019 -0400
@@ -17,8 +17,8 @@
 local Time = require "luan:Time.luan"
 local Rpc = require "luan:Rpc.luan"
 local LuceneIndex = require "java:luan.modules.lucene.LuceneIndex"
-local NumberFieldParser = require "java:luan.lib.queryparser.NumberFieldParser"
-local SaneQueryParser = require "java:luan.lib.queryparser.SaneQueryParser"
+local NumberFieldParser = require "java:goodjava.queryparser.NumberFieldParser"
+local SaneQueryParser = require "java:goodjava.queryparser.SaneQueryParser"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "Lucene"
 
--- a/src/luan/modules/lucene/LuceneIndex.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/lucene/LuceneIndex.java	Tue Sep 17 01:35:01 2019 -0400
@@ -68,12 +68,12 @@
 import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
 import org.apache.lucene.search.highlight.QueryScorer;
 import org.apache.lucene.search.highlight.TokenGroup;
-import luan.lib.queryparser.SaneQueryParser;
-import luan.lib.queryparser.FieldParser;
-import luan.lib.queryparser.MultiFieldParser;
-import luan.lib.queryparser.StringFieldParser;
-import luan.lib.queryparser.NumberFieldParser;
-import luan.lib.parser.ParseException;
+import goodjava.queryparser.SaneQueryParser;
+import goodjava.queryparser.FieldParser;
+import goodjava.queryparser.MultiFieldParser;
+import goodjava.queryparser.StringFieldParser;
+import goodjava.queryparser.NumberFieldParser;
+import goodjava.parser.ParseException;
 import luan.modules.Utils;
 import luan.Luan;
 import luan.LuanTable;
@@ -81,8 +81,8 @@
 import luan.LuanException;
 import luan.LuanRuntimeException;
 import luan.modules.parsers.LuanToString;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 
 
 public final class LuceneIndex {
--- a/src/luan/modules/lucene/PostgresBackup.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/lucene/PostgresBackup.java	Tue Sep 17 01:35:01 2019 -0400
@@ -17,8 +17,8 @@
 import luan.LuanException;
 import luan.modules.Utils;
 import luan.modules.parsers.LuanToString;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 
 
 final class PostgresBackup {
--- a/src/luan/modules/parsers/BBCode.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/parsers/BBCode.java	Tue Sep 17 01:35:01 2019 -0400
@@ -7,7 +7,7 @@
 import luan.LuanException;
 import luan.modules.Utils;
 import luan.modules.HtmlLuan;
-import luan.lib.parser.Parser;
+import goodjava.parser.Parser;
 
 
 public final class BBCode {
--- a/src/luan/modules/parsers/Css.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/parsers/Css.java	Tue Sep 17 01:35:01 2019 -0400
@@ -3,7 +3,7 @@
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanException;
-import luan.lib.parser.Parser;
+import goodjava.parser.Parser;
 
 
 public final class Css {
--- a/src/luan/modules/parsers/Csv.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/parsers/Csv.java	Tue Sep 17 01:35:01 2019 -0400
@@ -3,8 +3,8 @@
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanException;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
 
 
 public final class Csv {
--- a/src/luan/modules/parsers/Html.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/parsers/Html.java	Tue Sep 17 01:35:01 2019 -0400
@@ -7,7 +7,7 @@
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanException;
-import luan.lib.parser.Parser;
+import goodjava.parser.Parser;
 
 
 public final class Html {
--- a/src/luan/modules/parsers/Theme.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/parsers/Theme.java	Tue Sep 17 01:35:01 2019 -0400
@@ -1,8 +1,8 @@
 package luan.modules.parsers;
 
 import luan.LuanException;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
 
 
 public final class Theme {
--- a/src/luan/modules/sql/Database.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/sql/Database.java	Tue Sep 17 01:35:01 2019 -0400
@@ -9,8 +9,8 @@
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Properties;
-import luan.lib.logging.Logger;
-import luan.lib.logging.LoggerFactory;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanException;
--- a/src/luan/modules/url/LuanUrl.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/url/LuanUrl.java	Tue Sep 17 01:35:01 2019 -0400
@@ -17,7 +17,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Base64;
-import luan.lib.parser.ParseException;
+import goodjava.parser.ParseException;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanJavaFunction;
--- a/src/luan/modules/url/MultipartClient.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/url/MultipartClient.java	Tue Sep 17 01:35:01 2019 -0400
@@ -9,7 +9,7 @@
 import java.util.HashMap;
 import luan.LuanTable;
 import luan.LuanException;
-import luan.lib.webserver.Request;
+import goodjava.webserver.Request;
 
 
 public final class MultipartClient {
--- a/src/luan/modules/url/WwwAuthenticate.java	Mon Sep 16 22:51:41 2019 -0400
+++ b/src/luan/modules/url/WwwAuthenticate.java	Tue Sep 17 01:35:01 2019 -0400
@@ -2,8 +2,8 @@
 
 import java.util.Map;
 import java.util.HashMap;
-import luan.lib.parser.Parser;
-import luan.lib.parser.ParseException;
+import goodjava.parser.Parser;
+import goodjava.parser.ParseException;
 
 
 public final class WwwAuthenticate {