From 0494b531083edf24f44733a7a97a3da7367b80f2 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sun, 21 Sep 2014 07:05:52 -0500 Subject: [PATCH 1/4] Enhancements to HTTPContext * Default port is no 80. * Support for HTTP Basic Authentication. * Support for HTTPS. * Support for Content-Type. Defaults to "applicaion/json" but also supports passing a map to be encoded as "application/x-www-form-urlencoded". --- project.properties | 6 +- src/main/com/jdbernard/net/HttpContext.groovy | 96 +++++++++++++++++-- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/project.properties b/project.properties index fafceaf..3d65818 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Sat, 21 Sep 2013 02:27:46 -0500 +#Sun, 21 Sep 2014 03:05:17 -0500 name=jdb-util -version=2.2 +version=2.3 lib.local=true -build.number=8 +build.number=9 diff --git a/src/main/com/jdbernard/net/HttpContext.groovy b/src/main/com/jdbernard/net/HttpContext.groovy index fbe3162..34e31ba 100644 --- a/src/main/com/jdbernard/net/HttpContext.groovy +++ b/src/main/com/jdbernard/net/HttpContext.groovy @@ -4,6 +4,10 @@ package com.jdbernard.net import java.net.Socket +import java.net.URLEncoder +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.xml.bind.DatatypeConverter import groovy.json.JsonBuilder import groovy.json.JsonSlurper @@ -15,19 +19,34 @@ public class HttpContext { public String host = 'vbs-test' /** The port number.*/ - public int port = 8000 + public int port = 80 /** A cookie value to send in the request. This value will be automatically * set from any `Set-Cookie` header in the request response. */ public String cookie + /** HTTP Basic Authentication information. If this is a string, it will be + * Base64 encoded as-is. Otherwise it may be an object with "username" and + * "password" properties. The username and password will be concatenated + * with a colon in between. The result will be Base64 encoded. */ + public def basicAuth + + /** Set this to `true` to use HTTPS. Otherwise, HTTP will be used. */ + public boolean secure = false + + /** The default Content-Type that we will send with our requests. This can + * be overridden using the different method overloads. */ + public String defaultContentType = "application/json" + + private SSLSocketFactory sslSocketFactory = null; + /** #### makeMessage * Make and return an HTTP request for the given HTTP method and URL. If * `request` is not null, it is expected to be an object which will be * converted to JSON and included as the request body. The `Host`, * `Cookie`, and `Content-Length` headers will be added based on the * `host` and `cookie` fields and the `request` object. */ - public String makeMessage(String method, String url, def request) { + public String makeMessage(String method, String url, String contentType, def request) { StringBuilder message = new StringBuilder() message.append(method) message.append(" ") @@ -46,24 +65,60 @@ public class HttpContext { message.append(cookie) message.append("\r\n") } + if (basicAuth) { + message.append("Authorization: Basic ") + + if (basicAuth instanceof String) message.append( + DatatypeConverter.printBase64Binary( + (basicAuth as String).bytes)) + + else message.append( + DatatypeConverter.printBase64Binary( + "${basicAuth.username ?: ''}:${basicAuth.password ?: ''}".bytes)) + + message.append("\r\n") } + + if (contentType) { + message.append("Content-Type: ") + message.append(contentType) + message.append("\r\n") } + if (request) { - def jsonRequestBuilder = new JsonBuilder(request) - String requestBody = jsonRequestBuilder.toString() + String requestBody + if (contentType.startsWith("application/json") && + request instanceof Map) { + def jsonRequestBuilder = new JsonBuilder(request) + requestBody = jsonRequestBuilder.toString() } + + else if (contentType.startsWith("application/x-www-form-urlencoded") && + request instanceof Map) + requestBody = urlEncode(request) + + else requestBody = request.toString() message.append("Content-Length: ") message.append(requestBody.length()) message.append("\r\n\r\n") - message.append(requestBody) } + message.append(requestBody) + + message.append("\r\n") } + else message.append("\r\n") - message.append("\r\n") return message.toString() } + /** #### send + * A wrapper around `makeMessage` and `send(String message)` to create a + * request, send it, and return the response. This version allows you to + * specify the request's Content-Type.*/ + public def send(String method, String url, String contentType, def request) { + return send(makeMessage(method, url, contentType, request)) } + /** #### send * A wrapper around `makeMessage` and `send(String message)` to create a * request, send it, and return the response. */ public def send(String method, String url, def request) { - return send(makeMessage(method, url, request)) } + return send(makeMessage(method, url, defaultContentType, request)) } /** #### send * Send a message to the host specified by the object's `host` and `port` @@ -79,7 +134,18 @@ public class HttpContext { */ public def send(String message) { Map result = [headers:[], content: null] - Socket sock = new Socket(host, port) + + Socket sock + + if (secure) { + if (!sslSocketFactory) + sslSocketFactory = SSLSocketFactory.getDefault() + + sock = sslSocketFactory.createSocket(host, port) + } + + else sock = new Socket(host, port) + def startTime sock.withStreams { strin, out -> @@ -164,4 +230,18 @@ public class HttpContext { * `send(String method, String url, def request)` with `method = * "POST"`. */ public def post(String url, def body) { return send('POST', url, body) } + + /** #### post + * A wrapper to perform a `POST` request. This calls + * `send(String method, String url, def request)` with `method = + * "POST"`. This version also allows you to set the request's + * Content-Type. */ + public def post(String url, String contentType, def body) { + return send('POST', url, contentType, body) } + + private String urlEncode(Map m) { + List parts = m.collect { k, v -> + "$k=${URLEncoder.encode(v.toString(), 'UTF-8')}" } + return parts.join("&") } + } From 8ab35b27bddf1898c360d1c598e81eb7cbc2f6d1 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Thu, 25 Sep 2014 02:33:26 -0500 Subject: [PATCH 2/4] HttpContext response return integer status values, not strings. --- project.properties | 6 +++--- src/main/com/jdbernard/net/HttpContext.groovy | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/project.properties b/project.properties index 3d65818..c9271c1 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Sun, 21 Sep 2014 03:05:17 -0500 +#Thu, 25 Sep 2014 02:29:18 -0500 name=jdb-util -version=2.3 +version=2.4 lib.local=true -build.number=9 +build.number=1 diff --git a/src/main/com/jdbernard/net/HttpContext.groovy b/src/main/com/jdbernard/net/HttpContext.groovy index 34e31ba..3ebb8a7 100644 --- a/src/main/com/jdbernard/net/HttpContext.groovy +++ b/src/main/com/jdbernard/net/HttpContext.groovy @@ -157,7 +157,7 @@ public class HttpContext { writer.flush() def line = reader.readLine().trim() - result.status = line.split(/\s/)[1] + result.status = line.split(/\s/)[1] as int line = reader.readLine().trim() boolean isChunked = false From 4ab4e5876b04549b836465a0738669d48b090d27 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Tue, 28 Oct 2014 01:58:51 -0500 Subject: [PATCH 3/4] Rewrite LightOptionParser to support multiple parameter arguments. * LightOptionParser will only make one pass through the arguments array. * Make LOP support multiple instances of an option (-i in1 -i in2) * Make LOP support indeterminate option argument lengths (using arguments: "variable" in the definition). * Add unit tests for LightOptionParser and a testing phase during the `package` build target. --- build.xml | 2 +- project.properties | 4 +- .../jdbernard/util/LightOptionParser.groovy | 126 +++++++++++------- .../util/LightOptionParserTests.groovy | 65 +++++++++ 4 files changed, 146 insertions(+), 51 deletions(-) create mode 100644 src/test/com/jdbernard/util/LightOptionParserTests.groovy diff --git a/build.xml b/build.xml index dda64f7..684935a 100644 --- a/build.xml +++ b/build.xml @@ -4,7 +4,7 @@ - + diff --git a/project.properties b/project.properties index c9271c1..abe9784 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Thu, 25 Sep 2014 02:29:18 -0500 +#Tue, 28 Oct 2014 01:58:09 -0500 name=jdb-util -version=2.4 +version=3.0 lib.local=true build.number=1 diff --git a/src/main/com/jdbernard/util/LightOptionParser.groovy b/src/main/com/jdbernard/util/LightOptionParser.groovy index ffd36a6..d968c33 100644 --- a/src/main/com/jdbernard/util/LightOptionParser.groovy +++ b/src/main/com/jdbernard/util/LightOptionParser.groovy @@ -11,59 +11,97 @@ public class LightOptionParser { public static def parseOptions(def optionDefinitions, List args) { - def returnOpts = [:] - def foundOpts = [:] - def optionArgIndices = [] + def returnOpts = [args:[]] - /// Find all the options. - args.eachWithIndex { arg, idx -> - if (arg.startsWith('--')) foundOpts[arg.substring(2)] = [idx: idx] - else if (arg.startsWith('-')) foundOpts[arg.substring(1)] = [idx: idx] } + /// Look through each of the arguments to see if it is an option. + /// Note that we are manually advancing the index in the loop. + for (int i = 0; i < args.size();) { - /// Look for option arguments. - foundOpts.each { foundName, optInfo -> + def retVal = false + def optName = false - def retVal + if (args[i].startsWith('--')) optName = args[i].substring(2) + else if (args[i].startsWith('-')) optName = args[i].substring(1) - /// Find the definition for this option. - def optDef = optionDefinitions.find { - it.key == foundName || it.value.longName == foundName } + /// This was recognized as an option, try to find the definition + /// and read in any arguments. + if (optName) { - if (!optDef) throw new IllegalArgumentException( - "Unrecognized option: '${args[optInfo.idx]}.") + /// Find the definition for this option. + def optDef = optionDefinitions.find { + it.key == optName || it.value.longName == optName } - def optName = optDef.key - optDef = optDef.value + if (!optDef) throw new IllegalArgumentException( + "Unrecognized option: '${args[i]}'.") - /// Remember the option index for later. - optionArgIndices << optInfo.idx + optName = optDef.key + optDef = optDef.value - /// If there are no arguments, this is a flag. - if ((optDef.arguments ?: 0) == 0) retVal = true + /// If there are no arguments, this is a flag. Set the value + /// and advance the index. + if ((optDef.arguments ?: 0) == 0) { retVal = true; i++ } - /// Otherwise, read in the arguments - if (optDef.arguments && optDef.arguments > 0) { + /// If there are a pre-determined number of arguments, read them + /// in. + else if (optDef.arguments && + optDef.arguments instanceof Number && + optDef.arguments > 0) { - /// Not enough arguments left - if ((optInfo.idx + optDef.arguments) >= args.size()) { - throw new Exception("Option '${args[optInfo.idx]}' " + - "expects ${optDef.arguments} arguments.") } + retVal = [] - int firstArgIdx = optInfo.idx + 1 + /// Not enough arguments left + if ((i + optDef.arguments) >= args.size()) { + throw new Exception("Option '${args[i]}' " + + "expects ${optDef.arguments} arguments.") } - /// Case of only one argument - if (optDef.arguments == 1) - retVal = args[firstArgIdx] - /// Case of multiple arguments - else retVal = args[firstArgIdx..<(firstArgIdx + optDef.arguments)] + /// Advance past the option onto the first argument. + i++ - /// Remember all the option argument indices for later. - (firstArgIdx..<(firstArgIdx + optDef.arguments)).each { - optionArgIndices << it }} + /// Copy the arguments + retVal += args[i..<(i + optDef.arguments)] - /// Store the value in the returnOpts map - returnOpts[optName] = retVal - if (optDef.longName) returnOpts[optDef.longName] = retVal } + /// Advance the index past end of the arguements + i += optDef.arguments } + + /// If there are a variable number of arguments, treat all + /// arguments until the next argument or the end of options as + /// arguments for this option + else if (optDef.arguments == 'variable') { + + retVal = [] + + /// Advance past the option to the first argument + i++ + + /// As long as we have not hit another option or the end of + /// arguments, keep adding arguments to the list for this + /// option. + for(;i < args.size() && !args[i].startsWith('-'); i++) + retVal << args[i] } + + else { + throw new Exception("Invalid number of arguments " + + "defined for option ${optName}. The number of " + + "arguments must be either an integer or the value " + + "'variable'") } + + /// Set the value on the option. + if (retVal instanceof Boolean) { + returnOpts[optName] = retVal + if (optDef.longName) returnOpts[optDef.longName] = retVal } + + else { + if (!returnOpts.containsKey(optName)) + returnOpts[optName] = [] + returnOpts[optName] += retVal + + if (optDef.longName) { + if (!returnOpts.containsKey(optDef.longName)) + returnOpts[optDef.longName] = [] + returnOpts[optDef.longName] += retVal } } } + + /// This was not as option, it is an unclaomed argument. + else { returnOpts.args << args[i]; i++ } } /// Check that all required options have been found. optionDefinitions.each { optName, optDef -> @@ -71,17 +109,9 @@ public class LightOptionParser { if (optDef.required && /// and it has not been found, by either it's short or long name. !(returnOpts[optName] || - (optDef.longName && returnOpts[longName]))) + (optDef.longName && returnOpts[optDef.longName]))) throw new Exception("Missing required option: '-${optName}'.") } - /// Remove all the option arguments from the args list and return just - /// the non-option arguments. - optionArgIndices.sort().reverse().each { args.remove(it) } - - //optionArgIndices = optionArgIndices.collect { args[it] } - //args.removeAll(optionArgIndices) - - returnOpts.args = args return returnOpts } } diff --git a/src/test/com/jdbernard/util/LightOptionParserTests.groovy b/src/test/com/jdbernard/util/LightOptionParserTests.groovy new file mode 100644 index 0000000..763fdd3 --- /dev/null +++ b/src/test/com/jdbernard/util/LightOptionParserTests.groovy @@ -0,0 +1,65 @@ +package com.jdbernard.util + +import groovy.util.GroovyTestCase + +import org.junit.Test + +import static com.jdbernard.util.LightOptionParser.parseOptions + +public class LightOptionParserTests extends GroovyTestCase { + + def helpDef = ['h': [longName: 'help']] + def confDef = ['c': [longName: 'config', required: true, arguments: 1]] + def fullDef = [ + h: [longName: 'help'], + c: [longName: 'config-file', required: true, arguments: 1], + i: [longName: 'input-file', arguments: 'variable'], + o: [longName: 'output-file2', arguments: 2]] + + void testShortFlagPresent1() { assert parseOptions(helpDef, ["-h"]).h } + void testShortFlagPresent2() { assert parseOptions(helpDef, ["-h"]).help } + void testLongFlagPresent() { assert parseOptions(helpDef, ["--help"]).h} + void testShortFlagPresent() { assert parseOptions(helpDef, ["--help"]).help } + + void testFlagAbsent1() { assert !parseOptions(helpDef, ["arg"]).h } + void testFlagAbsent2() { assert !parseOptions(helpDef, ["arg"]).help } + + void testRequiredOptionMissing() { + try { + parseOptions(confDef, ["arg"]) + assert false } + catch (Exception e) {} } + + void testSingleArg1() { + assert parseOptions(confDef, ["-c", "confFile"]).c == ["confFile"] } + + void testSingleArg2() { + assert parseOptions(confDef, ["-c", "confFile"]).config == ["confFile"] } + + void testUnclaimedArgsAndFlag() { + def opts = parseOptions(helpDef, ["arg1", "-h", "arg2"]) + assert opts.args == ["arg1", "arg2"] } + + void testUnclaimedAndClaimedArgs() { + def opts = parseOptions(fullDef, ["-c", "confFile", "arg1"]) + assert opts.args == ["arg1"] + assert opts.c == ["confFile"] } + + /*void testMultipleArgs1() { + def opts = parseOptions(fullDef, ["-c", "confFile", ""]) + assert .conf == ["confFile"] }*/ + + void testFull() { + def opts = parseOptions(fullDef, + ["-c", "cfgFile", "arg1", "-i", "in1", "in2", "in3", + "-o", "out1", "out2", "arg2", "-h", "-i", "in4"]) + + assert opts.h + assert opts.c == ["cfgFile"] + assert opts['config-file'] == ["cfgFile"] + assert opts.args == ["arg1", "arg2"] + assert opts.i == ["in1", "in2", "in3", "in4"] + assert opts["input-file"] == ["in1", "in2", "in3", "in4"] + assert opts.o == ["out1", "out2"] + assert opts["output-file2"] == ["out1", "out2"] } +} From 14332b878ce29d57aee9bea47bc4e42ff4555612 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Wed, 29 Oct 2014 19:33:04 -0500 Subject: [PATCH 4/4] Added a LightOptionParser.parseOptions overload that accepts String[]. --- project.properties | 6 +++--- src/main/com/jdbernard/util/LightOptionParser.groovy | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/project.properties b/project.properties index abe9784..06f4ecc 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Tue, 28 Oct 2014 01:58:09 -0500 +#Wed, 29 Oct 2014 19:31:44 -0500 name=jdb-util -version=3.0 +version=3.1 lib.local=true -build.number=1 +build.number=0 diff --git a/src/main/com/jdbernard/util/LightOptionParser.groovy b/src/main/com/jdbernard/util/LightOptionParser.groovy index d968c33..381b831 100644 --- a/src/main/com/jdbernard/util/LightOptionParser.groovy +++ b/src/main/com/jdbernard/util/LightOptionParser.groovy @@ -9,7 +9,10 @@ package com.jdbernard.util public class LightOptionParser { - public static def parseOptions(def optionDefinitions, List args) { + public static def parseOptions(def optionDefinitions, String[] args) { + return parseOptions(optionDefinitions, args as List) } + + public static def parseOptions(def optionDefinitions, List args) { def returnOpts = [args:[]]