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"] }
+}