changeset 725:a741a3a33423

add url support for multipart/form-data
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 08 Jun 2016 23:13:10 -0600
parents 4f8e30a3ffd0
children 14f136a4641f
files core/src/luan/modules/IoLuan.java core/src/luan/modules/LuanUrl.java core/src/luan/modules/url/LuanUrl.java core/src/luan/modules/url/MultiPartOutputStream.java core/src/luan/modules/url/MultipartClient.java http/src/luan/modules/http/HttpServicer.java
diffstat 6 files changed, 512 insertions(+), 269 deletions(-) [+]
line wrap: on
line diff
--- a/core/src/luan/modules/IoLuan.java	Tue Jun 07 16:00:41 2016 -0600
+++ b/core/src/luan/modules/IoLuan.java	Wed Jun 08 23:13:10 2016 -0600
@@ -36,6 +36,7 @@
 import luan.LuanFunction;
 import luan.LuanJavaFunction;
 import luan.LuanException;
+import luan.modules.url.LuanUrl;
 
 
 public final class IoLuan {
@@ -167,7 +168,7 @@
 
 
 	public static abstract class LuanIn {
-		abstract InputStream inputStream() throws IOException, LuanException;
+		public abstract InputStream inputStream() throws IOException, LuanException;
 		public abstract String to_string();
 		public abstract String to_uri_string();
 
@@ -241,7 +242,7 @@
 
 	public static final LuanIn defaultStdin = new LuanIn() {
 
-		@Override InputStream inputStream() {
+		@Override public InputStream inputStream() {
 			return System.in;
 		}
 
@@ -324,7 +325,7 @@
 			@Override public void write(int b) {}
 		};
 
-		@Override InputStream inputStream() {
+		@Override public InputStream inputStream() {
 			return in;
 		}
 
@@ -349,7 +350,7 @@
 			this.s = s;
 		}
 
-		@Override InputStream inputStream() {
+		@Override public InputStream inputStream() {
 			throw new UnsupportedOperationException();
 		}
 
@@ -407,7 +408,7 @@
 			this.file = file;
 		}
 
-		@Override InputStream inputStream() throws IOException {
+		@Override public InputStream inputStream() throws IOException {
 			return new FileInputStream(file);
 		}
 
@@ -621,7 +622,7 @@
 			this.socket = socket;
 		}
 
-		@Override InputStream inputStream() throws IOException {
+		@Override public InputStream inputStream() throws IOException {
 			return socket.getInputStream();
 		}
 
--- a/core/src/luan/modules/LuanUrl.java	Tue Jun 07 16:00:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,262 +0,0 @@
-package luan.modules;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.HttpURLConnection;
-import java.net.URLEncoder;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Base64;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanJavaFunction;
-import luan.LuanException;
-
-
-public final class LuanUrl extends IoLuan.LuanIn {
-	private static enum Method { GET, POST, DELETE }
-
-	private URL url;
-	private Method method = Method.GET;
-	private Map headers;
-	private String content;
-
-	LuanUrl(LuanState luan,URL url,LuanTable options) throws LuanException {
-		this.url = url;
-		if( options != null ) {
-			Map map = options.asMap(luan);
-			String methodStr = getString(map,"method");
-			if( methodStr != null ) {
-				methodStr = methodStr.toUpperCase();
-				try {
-					this.method = Method.valueOf(methodStr);
-				} catch(IllegalArgumentException e) {
-					throw new LuanException( "invalid method: "+methodStr );
-				}
-			}
-			Map headerMap = getMap(luan,map,"headers");
-			if( headerMap != null ) {
-				headers = new HashMap();
-				for( Object hack : headerMap.entrySet() ) {
-					Map.Entry entry = (Map.Entry)hack;
-					String key = (String)entry.getKey();
-					Object val = entry.getValue();
-					String name = toHttpHeaderName(key);
-					if( val instanceof String ) {
-						headers.put(name,val);
-					} else {
-						if( !(val instanceof LuanTable) )
-							throw new LuanException( "header '"+key+"' must be string or table" );
-						LuanTable t = (LuanTable)val;
-						if( !t.isList() )
-							throw new LuanException( "header '"+key+"' table must be list" );
-						headers.put(name,t.asList());
-					}
-				}
-			}
-			Map auth = getMap(luan,map,"authorization");
-			if( auth != null ) {
-				if( headers!=null && headers.containsKey("Authorization") )
-					throw new LuanException( "can't define authorization with header 'Authorization' defined" );
-				String user = getString(auth,"user");
-				String password = getString(auth,"password");
-				if( !auth.isEmpty() )
-					throw new LuanException( "unrecognized authorization options: "+auth );
-				StringBuilder sb = new StringBuilder();
-				if( user != null )
-					sb.append(user);
-				sb.append(':');
-				if( password != null )
-					sb.append(password);
-				String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes());
-				if( headers == null )
-					headers = new HashMap();
-				headers.put("Authorization",val);
-			}
-			Map params = getMap(luan,map,"parameters");
-			if( params != null ) {
-				StringBuilder sb = new StringBuilder();
-				for( Object hack : params.entrySet() ) {
-					Map.Entry entry = (Map.Entry)hack;
-					String key = (String)entry.getKey();
-					Object val = entry.getValue();
-					String keyEnc = encode(key);
-					if( val instanceof String ) {
-						and(sb);
-						sb.append( keyEnc ).append( '=' ).append( encode((String)val) );
-					} else {
-						if( !(val instanceof LuanTable) )
-							throw new LuanException( "parameter '"+key+"' must be string or table" );
-						LuanTable t = (LuanTable)val;
-						if( !t.isList() )
-							throw new LuanException( "parameter '"+key+"' table must be list" );
-						for( Object obj : t.asList() ) {
-							if( !(obj instanceof String) )
-								throw new LuanException( "parameter '"+key+"' values must be strings" );
-							and(sb);
-							sb.append( keyEnc ).append( '=' ).append( encode((String)obj) );
-						}
-					}
-				}
-				if( this.method==Method.DELETE )
-					throw new LuanException( "the DELETE method cannot take parameters" );
-				if( this.method==Method.POST ) {
-					content = sb.toString();
-				} else { // GET
-					String urlS = this.url.toString();
-					if( urlS.indexOf('?') == -1 ) {
-						urlS += '?';
-					} else {
-						urlS += '&';
-					}
-					urlS += sb;
-					try {
-						this.url = new URL(urlS);
-					} catch(IOException e) {
-						throw new RuntimeException(e);
-					}
-				}
-			}
-			if( !map.isEmpty() )
-				throw new LuanException( "unrecognized options: "+map );
-		}
-	}
-
-	public static String toHttpHeaderName(String luanName) {
-		luanName = luanName.toLowerCase();
-		StringBuilder buf = new StringBuilder();
-		boolean capitalize = true;
-		char[] a = luanName.toCharArray();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			if( c == '_'  || c == '-' ) {
-				a[i] = '-';
-				capitalize = true;
-			} else if( capitalize ) {
-				a[i] = Character.toUpperCase(c);
-				capitalize = false;
-			}
-		}
-		return String.valueOf(a);
-	}
-
-	private static void and(StringBuilder sb) {
-		if( sb.length() > 0 )
-			sb.append('&');
-	}
-
-	private static String encode(String s) {
-		try {
-			return URLEncoder.encode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	private static String getString(Map map,String key) throws LuanException {
-		Object val = map.remove(key);
-		if( val!=null && !(val instanceof String) )
-			throw new LuanException( "parameter '"+key+"' must be a string" );
-		return (String)val;
-	}
-
-	private static LuanTable getTable(Map map,String key) throws LuanException {
-		Object val = map.remove(key);
-		if( val!=null && !(val instanceof LuanTable) )
-			throw new LuanException( "parameter '"+key+"' must be a table" );
-		return (LuanTable)val;
-	}
-
-	private static Map getMap(LuanState luan,Map map,String key) throws LuanException {
-		LuanTable t = getTable(map,key);
-		return t==null ? null : t.asMap(luan);
-	}
-
-	@Override InputStream inputStream() throws IOException, LuanException {
-		URLConnection con = url.openConnection();
-		if( headers != null ) {
-			for( Object hack : headers.entrySet() ) {
-				Map.Entry entry = (Map.Entry)hack;
-				String key = (String)entry.getKey();
-				Object val = entry.getValue();
-				if( val instanceof String ) {
-					con.addRequestProperty(key,(String)val);
-				} else {
-					List list = (List)val;
-					for( Object obj : list ) {
-						con.addRequestProperty(key,(String)obj);
-					}
-				}
-			}
-		}
-		if( method==Method.GET ) {
-			return con.getInputStream();
-		}
-
-		HttpURLConnection httpCon = (HttpURLConnection)con;
-
-		if( method==Method.DELETE ) {
-			httpCon.setRequestMethod("DELETE");
-			return httpCon.getInputStream();
-		}
-
-		// POST
-
-//		httpCon.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
-		httpCon.setDoOutput(true);
-		httpCon.setRequestMethod("POST");
-
-		byte[] post = content.getBytes();
-//		httpCon.setRequestProperty("Content-Length",Integer.toString(post.length));
-		OutputStream out = httpCon.getOutputStream();
-		out.write(post);
-		out.flush();
-		try {
-			try {
-				return httpCon.getInputStream();
-			} catch(IOException e) {
-				InputStream is = httpCon.getErrorStream();
-				if( is == null )
-					throw e;
-				Reader in = new InputStreamReader(is);
-				String msg = Utils.readAll(in);
-				in.close();
-				throw new LuanException(msg,e);
-			}
-		} finally {
-			out.close();
-		}
-	}
-
-	@Override public String to_string() {
-		return url.toString();
-	}
-
-	@Override public String to_uri_string() {
-		return url.toString();
-	}
-/*
-	public String post(String postS) throws IOException {
-		return new UrlCall(url).post(postS);
-	}
-
-	@Override public LuanTable table() {
-		LuanTable tbl = super.table();
-		try {
-			tbl.rawPut( "post", new LuanJavaFunction(
-				LuanUrl.class.getMethod( "post", String.class ), this
-			) );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-		return tbl;
-	}
-*/
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/src/luan/modules/url/LuanUrl.java	Wed Jun 08 23:13:10 2016 -0600
@@ -0,0 +1,275 @@
+package luan.modules.url;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.HttpURLConnection;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Base64;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanJavaFunction;
+import luan.LuanException;
+import luan.modules.IoLuan;
+import luan.modules.Utils;
+
+
+public final class LuanUrl extends IoLuan.LuanIn {
+
+	private static enum Method { GET, POST, DELETE }
+
+	private URL url;
+	private Method method = Method.GET;
+	private Map headers;
+	private String content = null;
+	private MultipartClient multipart = null;
+
+	public LuanUrl(LuanState luan,URL url,LuanTable options) throws LuanException {
+		this.url = url;
+		if( options != null ) {
+			Map map = options.asMap(luan);
+			String methodStr = getString(map,"method");
+			if( methodStr != null ) {
+				methodStr = methodStr.toUpperCase();
+				try {
+					this.method = Method.valueOf(methodStr);
+				} catch(IllegalArgumentException e) {
+					throw new LuanException( "invalid method: "+methodStr );
+				}
+			}
+			Map headerMap = getMap(luan,map,"headers");
+			if( headerMap != null ) {
+				headers = new HashMap();
+				for( Object hack : headerMap.entrySet() ) {
+					Map.Entry entry = (Map.Entry)hack;
+					String key = (String)entry.getKey();
+					Object val = entry.getValue();
+					String name = toHttpHeaderName(key);
+					if( val instanceof String ) {
+						headers.put(name,val);
+					} else {
+						if( !(val instanceof LuanTable) )
+							throw new LuanException( "header '"+key+"' must be string or table" );
+						LuanTable t = (LuanTable)val;
+						if( !t.isList() )
+							throw new LuanException( "header '"+key+"' table must be list" );
+						headers.put(name,t.asList());
+					}
+				}
+			}
+			Map auth = getMap(luan,map,"authorization");
+			if( auth != null ) {
+				if( headers!=null && headers.containsKey("Authorization") )
+					throw new LuanException( "can't define authorization with header 'Authorization' defined" );
+				String user = getString(auth,"user");
+				String password = getString(auth,"password");
+				if( !auth.isEmpty() )
+					throw new LuanException( "unrecognized authorization options: "+auth );
+				StringBuilder sb = new StringBuilder();
+				if( user != null )
+					sb.append(user);
+				sb.append(':');
+				if( password != null )
+					sb.append(password);
+				String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes());
+				if( headers == null )
+					headers = new HashMap();
+				headers.put("Authorization",val);
+			}
+			Map params = getMap(luan,map,"parameters");
+			if( params != null ) {
+				if( this.method==Method.POST && "multipart/form-data".equals(headers.get("Content-Type")) ) {
+					multipart = new MultipartClient(params);
+				} else {
+					StringBuilder sb = new StringBuilder();
+					for( Object hack : params.entrySet() ) {
+						Map.Entry entry = (Map.Entry)hack;
+						String key = (String)entry.getKey();
+						Object val = entry.getValue();
+						String keyEnc = encode(key);
+						if( val instanceof String ) {
+							and(sb);
+							sb.append( keyEnc ).append( '=' ).append( encode((String)val) );
+						} else {
+							if( !(val instanceof LuanTable) )
+								throw new LuanException( "parameter '"+key+"' must be string or table" );
+							LuanTable t = (LuanTable)val;
+							if( !t.isList() )
+								throw new LuanException( "parameter '"+key+"' table must be list" );
+							for( Object obj : t.asList() ) {
+								if( !(obj instanceof String) )
+									throw new LuanException( "parameter '"+key+"' values must be strings" );
+								and(sb);
+								sb.append( keyEnc ).append( '=' ).append( encode((String)obj) );
+							}
+						}
+					}
+					if( this.method==Method.DELETE )
+						throw new LuanException( "the DELETE method cannot take parameters" );
+					if( this.method==Method.POST ) {
+						content = sb.toString();
+					} else { // GET
+						String urlS = this.url.toString();
+						if( urlS.indexOf('?') == -1 ) {
+							urlS += '?';
+						} else {
+							urlS += '&';
+						}
+						urlS += sb;
+						try {
+							this.url = new URL(urlS);
+						} catch(IOException e) {
+							throw new RuntimeException(e);
+						}
+					}
+				}
+			}
+			if( !map.isEmpty() )
+				throw new LuanException( "unrecognized options: "+map );
+		}
+	}
+
+	public static String toHttpHeaderName(String luanName) {
+		luanName = luanName.toLowerCase();
+		StringBuilder buf = new StringBuilder();
+		boolean capitalize = true;
+		char[] a = luanName.toCharArray();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			if( c == '_'  || c == '-' ) {
+				a[i] = '-';
+				capitalize = true;
+			} else if( capitalize ) {
+				a[i] = Character.toUpperCase(c);
+				capitalize = false;
+			}
+		}
+		return String.valueOf(a);
+	}
+
+	private static void and(StringBuilder sb) {
+		if( sb.length() > 0 )
+			sb.append('&');
+	}
+
+	private static String encode(String s) {
+		try {
+			return URLEncoder.encode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private static String getString(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val!=null && !(val instanceof String) )
+			throw new LuanException( "parameter '"+key+"' must be a string" );
+		return (String)val;
+	}
+
+	private static LuanTable getTable(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val!=null && !(val instanceof LuanTable) )
+			throw new LuanException( "parameter '"+key+"' must be a table" );
+		return (LuanTable)val;
+	}
+
+	private static Map getMap(LuanState luan,Map map,String key) throws LuanException {
+		LuanTable t = getTable(map,key);
+		return t==null ? null : t.asMap(luan);
+	}
+
+	@Override public InputStream inputStream() throws IOException, LuanException {
+		URLConnection con = url.openConnection();
+		if( headers != null ) {
+			for( Object hack : headers.entrySet() ) {
+				Map.Entry entry = (Map.Entry)hack;
+				String key = (String)entry.getKey();
+				Object val = entry.getValue();
+				if( val instanceof String ) {
+					con.addRequestProperty(key,(String)val);
+				} else {
+					List list = (List)val;
+					for( Object obj : list ) {
+						con.addRequestProperty(key,(String)obj);
+					}
+				}
+			}
+		}
+		if( method==Method.GET ) {
+			return con.getInputStream();
+		}
+
+		HttpURLConnection httpCon = (HttpURLConnection)con;
+
+		if( method==Method.DELETE ) {
+			httpCon.setRequestMethod("DELETE");
+			return httpCon.getInputStream();
+		}
+
+		// POST
+
+//		httpCon.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
+		httpCon.setDoOutput(true);
+		httpCon.setRequestMethod("POST");
+
+		OutputStream out;
+		if( multipart != null ) {
+			out = multipart.write(httpCon);
+		} else {
+			byte[] post = content.getBytes();
+//			httpCon.setRequestProperty("Content-Length",Integer.toString(post.length));
+			out = httpCon.getOutputStream();
+			out.write(post);
+			out.flush();
+		}
+		try {
+			try {
+				return httpCon.getInputStream();
+			} catch(IOException e) {
+				InputStream is = httpCon.getErrorStream();
+				if( is == null )
+					throw e;
+				Reader in = new InputStreamReader(is);
+				String msg = Utils.readAll(in);
+				in.close();
+				throw new LuanException(msg,e);
+			}
+		} finally {
+			out.close();
+		}
+	}
+
+	@Override public String to_string() {
+		return url.toString();
+	}
+
+	@Override public String to_uri_string() {
+		return url.toString();
+	}
+/*
+	public String post(String postS) throws IOException {
+		return new UrlCall(url).post(postS);
+	}
+
+	@Override public LuanTable table() {
+		LuanTable tbl = super.table();
+		try {
+			tbl.rawPut( "post", new LuanJavaFunction(
+				LuanUrl.class.getMethod( "post", String.class ), this
+			) );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+		return tbl;
+	}
+*/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/src/luan/modules/url/MultiPartOutputStream.java	Wed Jun 08 23:13:10 2016 -0600
@@ -0,0 +1,146 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+//  This horrible broken code from jetty is just here for me to look at.  It isn't used.  -fschmidt
+
+package luan.modules.url;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+/* ================================================================ */
+/** Handle a multipart MIME response.
+ *
+ * 
+ * 
+*/
+public class MultiPartOutputStream extends FilterOutputStream
+{
+    /* ------------------------------------------------------------ */
+    private static final byte[] __CRLF={'\r','\n'};
+    private static final byte[] __DASHDASH={'-','-'};
+    
+    public static String MULTIPART_MIXED="multipart/mixed";
+    public static String MULTIPART_X_MIXED_REPLACE="multipart/x-mixed-replace";
+    public static final String __ISO_8859_1="ISO-8859-1";
+
+	public static String newBoundary(Object obj) {
+        return "jetty"+System.identityHashCode(obj)+
+        Long.toString(System.currentTimeMillis(),36);
+	}
+    
+    /* ------------------------------------------------------------ */
+    private final String boundary;
+    private final byte[] boundaryBytes;
+
+    /* ------------------------------------------------------------ */
+    private boolean inPart=false;    
+    
+    /* ------------------------------------------------------------ */
+    public MultiPartOutputStream(OutputStream out,String boundary)
+    throws IOException
+    {
+        super(out);
+
+		this.boundary = boundary;
+        boundaryBytes=boundary.getBytes(__ISO_8859_1);
+
+        inPart=false;
+    }
+
+    
+
+    /* ------------------------------------------------------------ */
+    /** End the current part.
+     * @exception IOException IOException
+     */
+    @Override
+    public void close()
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__DASHDASH);
+        out.write(__CRLF);
+        inPart=false;
+        super.close();
+    }
+    
+    /* ------------------------------------------------------------ */
+    public String getBoundary()
+    {
+        return boundary;
+    }
+
+    public OutputStream getOut() {return out;}
+    
+    /* ------------------------------------------------------------ */
+    /** Start creation of the next Content.
+     */
+    public void startPart(String contentType)
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        inPart=true;
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__CRLF);
+        if (contentType != null)
+            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
+        out.write(__CRLF);
+        out.write(__CRLF);
+    }
+        
+    /* ------------------------------------------------------------ */
+    /** Start creation of the next Content.
+     */
+    public void startPart(String contentType, String[] headers)
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        inPart=true;
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__CRLF);
+        if (contentType != null)
+            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
+        out.write(__CRLF);
+        for (int i=0;headers!=null && i<headers.length;i++)
+        {
+            out.write(headers[i].getBytes(__ISO_8859_1));
+            out.write(__CRLF);
+        }
+        out.write(__CRLF);
+    }
+    
+    /* ------------------------------------------------------------ */
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException
+    {
+        out.write(b,off,len);
+    }
+}
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/src/luan/modules/url/MultipartClient.java	Wed Jun 08 23:13:10 2016 -0600
@@ -0,0 +1,76 @@
+package luan.modules.url;
+
+import java.io.OutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import luan.LuanTable;
+import luan.LuanException;
+
+
+public final class MultipartClient {
+	private static final byte[] __CRLF = {'\r','\n'};
+	private static final byte[] __DASHDASH = {'-','-'};
+	private static final String __ISO_8859_1 = "ISO-8859-1";
+
+	private final Map params = new HashMap();
+
+	MultipartClient(Map params) throws LuanException {
+		for( Object hack : params.entrySet() ) {
+			Map.Entry entry = (Map.Entry)hack;
+			String key = (String)entry.getKey();
+			Object val = entry.getValue();
+			List list = new ArrayList();
+			if( val instanceof String ) {
+				list.add(val);
+			} else {
+				if( !(val instanceof LuanTable) )
+					throw new LuanException( "parameter '"+key+"' must be string or table" );
+				LuanTable t = (LuanTable)val;
+				if( !t.isList() )
+					throw new LuanException( "parameter '"+key+"' table must be list" );
+				for( Object obj : t.asList() ) {
+					if( !(obj instanceof String) )
+						throw new LuanException( "parameter '"+key+"' values must be strings" );
+					list.add(obj);
+				}
+			}
+			this.params.put(key,list);
+		}
+	}
+
+	public OutputStream write(HttpURLConnection httpCon) throws IOException {
+		String boundary = "luan" + System.identityHashCode(this) + Long.toString(System.currentTimeMillis(),36);
+		byte[] boundaryBytes = boundary.getBytes(__ISO_8859_1);
+
+		httpCon.setRequestProperty("Content-Type","multipart/form-data; boundary="+boundary);
+		OutputStream out = httpCon.getOutputStream();
+		for( Object hack : params.entrySet() ) {
+			Map.Entry entry = (Map.Entry)hack;
+			String name = (String)entry.getKey();
+			List list = (List)entry.getValue();
+			for( Object obj : list ) {
+				String val = (String)obj;
+		        out.write(__DASHDASH);
+		        out.write(boundaryBytes);
+		        out.write(__CRLF);
+//		        if (contentType != null)
+//		            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
+//		        out.write(__CRLF);
+	            out.write(("Content-Disposition: form-data; name=\""+name+"\"").getBytes(__ISO_8859_1));
+	            out.write(__CRLF);
+		        out.write(__CRLF);
+				out.write(val.getBytes());
+		        out.write(__CRLF);
+			}
+		}
+		out.write(__DASHDASH);
+		out.write(boundaryBytes);
+		out.write(__DASHDASH);
+		out.write(__CRLF);
+		return out;
+	}
+}
--- a/http/src/luan/modules/http/HttpServicer.java	Tue Jun 07 16:00:41 2016 -0600
+++ b/http/src/luan/modules/http/HttpServicer.java	Wed Jun 08 23:13:10 2016 -0600
@@ -33,9 +33,9 @@
 import luan.DeepCloner;
 import luan.modules.PackageLuan;
 import luan.modules.IoLuan;
-import luan.modules.LuanUrl;
 import luan.modules.TableLuan;
 import luan.modules.Utils;
+import luan.modules.url.LuanUrl;
 
 
 public final class HttpServicer {
@@ -108,6 +108,13 @@
 				for( Part p : mpis.getParts() ) {
 					final MultiPartInputStream.MultiPart part = (MultiPartInputStream.MultiPart)p;
 					String name = part.getName();
+/*
+System.out.println("name = "+name);
+System.out.println("getContentType = "+part.getContentType());
+System.out.println("getHeaderNames = "+part.getHeaderNames());
+System.out.println("content-disposition = "+part.getHeader("content-disposition"));
+System.out.println();
+*/
 					Object value;
 					String filename = part.getContentDispositionFilename();
 					if( filename == null ) {