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 78f330b..fc646b5 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,12 @@ +<<<<<<< HEAD #Sat, 11 Jan 2014 18:55:50 -0600 name=jdb-util version=2.3 +======= +#Wed, 29 Oct 2014 19:31:44 -0500 +name=jdb-util +version=3.1 +>>>>>>> 14332b878ce29d57aee9bea47bc4e42ff4555612 lib.local=true build.number=0 diff --git a/src/main/com/jdbernard/net/HttpContext.groovy b/src/main/com/jdbernard/net/HttpContext.groovy index b4d1186..5089b7c 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 (request) { - String requestBody = new JsonBuilder(request).toString() + 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) contentType = "application.json" + message.append("Content-Type: ") + message.append(contentType) + message.append("\r\n") + + if (request) { + 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-Type: application/json\r\n") 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 -> @@ -91,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 @@ -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("&") } + } diff --git a/src/main/com/jdbernard/util/LightOptionParser.groovy b/src/main/com/jdbernard/util/LightOptionParser.groovy index ffd36a6..381b831 100644 --- a/src/main/com/jdbernard/util/LightOptionParser.groovy +++ b/src/main/com/jdbernard/util/LightOptionParser.groovy @@ -9,61 +9,102 @@ 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) } - def returnOpts = [:] - def foundOpts = [:] - def optionArgIndices = [] + public static def parseOptions(def optionDefinitions, List 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] } + def returnOpts = [args:[]] - /// Look for option arguments. - foundOpts.each { foundName, optInfo -> + /// 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();) { - def retVal + def retVal = false + def optName = false - /// Find the definition for this option. - def optDef = optionDefinitions.find { - it.key == foundName || it.value.longName == foundName } + if (args[i].startsWith('--')) optName = args[i].substring(2) + else if (args[i].startsWith('-')) optName = args[i].substring(1) - if (!optDef) throw new IllegalArgumentException( - "Unrecognized option: '${args[optInfo.idx]}.") + /// This was recognized as an option, try to find the definition + /// and read in any arguments. + if (optName) { - def optName = optDef.key - optDef = optDef.value + /// Find the definition for this option. + def optDef = optionDefinitions.find { + it.key == optName || it.value.longName == optName } - /// Remember the option index for later. - optionArgIndices << optInfo.idx + if (!optDef) throw new IllegalArgumentException( + "Unrecognized option: '${args[i]}'.") - /// If there are no arguments, this is a flag. - if ((optDef.arguments ?: 0) == 0) retVal = true + optName = optDef.key + optDef = optDef.value - /// Otherwise, read in the arguments - if (optDef.arguments && optDef.arguments > 0) { + /// If there are no arguments, this is a flag. Set the value + /// and advance the index. + if ((optDef.arguments ?: 0) == 0) { retVal = true; i++ } - /// Not enough arguments left - if ((optInfo.idx + optDef.arguments) >= args.size()) { - throw new Exception("Option '${args[optInfo.idx]}' " + - "expects ${optDef.arguments} arguments.") } + /// If there are a pre-determined number of arguments, read them + /// in. + else if (optDef.arguments && + optDef.arguments instanceof Number && + optDef.arguments > 0) { - int firstArgIdx = optInfo.idx + 1 + retVal = [] - /// Case of only one argument - if (optDef.arguments == 1) - retVal = args[firstArgIdx] - /// Case of multiple arguments - else retVal = args[firstArgIdx..<(firstArgIdx + optDef.arguments)] + /// Not enough arguments left + if ((i + optDef.arguments) >= args.size()) { + throw new Exception("Option '${args[i]}' " + + "expects ${optDef.arguments} arguments.") } - /// Remember all the option argument indices for later. - (firstArgIdx..<(firstArgIdx + optDef.arguments)).each { - optionArgIndices << it }} + /// Advance past the option onto the first argument. + i++ - /// Store the value in the returnOpts map - returnOpts[optName] = retVal - if (optDef.longName) returnOpts[optDef.longName] = retVal } + /// Copy the arguments + retVal += args[i..<(i + optDef.arguments)] + + /// 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 +112,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"] } +}