changeset 834:5aa19bbed0d6 monetdbs

Comments and formatting
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Wed, 13 Dec 2023 15:39:47 +0100 (16 months ago)
parents a71afa48f269
children 071be9a628e8
files src/main/java/org/monetdb/client/JdbcClient.java src/main/java/org/monetdb/jdbc/MonetConnection.java src/main/java/org/monetdb/jdbc/MonetDriver.java src/main/java/org/monetdb/mcl/net/MapiSocket.java src/main/java/org/monetdb/mcl/net/MonetUrlParser.java src/main/java/org/monetdb/mcl/net/Parameter.java src/main/java/org/monetdb/mcl/net/ParameterType.java src/main/java/org/monetdb/mcl/net/SecureSocket.java src/main/java/org/monetdb/mcl/net/Target.java src/main/java/org/monetdb/mcl/net/ValidationError.java tests/TLSTester.java tests/UrlTester.java
diffstat 12 files changed, 2054 insertions(+), 1839 deletions(-) [+]
line wrap: on
line diff
--- a/src/main/java/org/monetdb/client/JdbcClient.java
+++ b/src/main/java/org/monetdb/client/JdbcClient.java
@@ -192,9 +192,6 @@ public final class JdbcClient {
 				"statements read.  Batching can greatly speedup the " +
 				"process of restoring a database dump.");
 
-//	This  file can contain defaults for the flags user, password, language,
-//	database, save_history, format, host, port, and width.  For example, an
-
 		copts.addIgnored("save_history");
 		copts.addIgnored("format");
 		copts.addIgnored("width");
--- a/src/main/java/org/monetdb/jdbc/MonetConnection.java
+++ b/src/main/java/org/monetdb/jdbc/MonetConnection.java
@@ -75,7 +75,7 @@ public class MonetConnection
 	extends MonetWrapper
 	implements Connection, AutoCloseable
 {
-	/** All connection parameters */
+	/* All connection parameters */
 	Target target;
 	/** A connection to mserver5 using a TCP socket */
 	private final MapiSocket server;
@@ -139,13 +139,11 @@ public class MonetConnection
 	private DatabaseMetaData dbmd;
 
 	/**
-	 * Constructor of a Connection for MonetDB. At this moment the
-	 * current implementation limits itself to storing the given host,
-	 * database, username and password for later use by the
-	 * createStatement() call.  This constructor is only accessible to
+	 * Constructor of a Connection for MonetDB.
+	 * This constructor is only accessible to
 	 * classes from the jdbc package.
 	 *
-	 * @param target a Target object holding the connection parameters
+	 * @param target a {@link Target} object containing all connection parameters
 	 * @throws SQLException if a database error occurs
 	 * @throws IllegalArgumentException is one of the arguments is null or empty
 	 */
@@ -184,20 +182,20 @@ public class MonetConnection
 		switch (target.getLanguage()) {
 			case "sql":
 				lang = LANG_SQL;
-				queryTempl[0] = "s";		// pre
-				queryTempl[1] = "\n;";		// post
-				queryTempl[2] = "\n;\n";	// separator
-				commandTempl[0] = "X";		// pre
-				commandTempl[1] = "";		// post
+				queryTempl[0] = "s";        // pre
+				queryTempl[1] = "\n;";      // post
+				queryTempl[2] = "\n;\n";    // separator
+				commandTempl[0] = "X";      // pre
+				commandTempl[1] = "";       // post
 				callback = new SqlOptionsCallback();
 				break;
 			case "mal":
 				lang = LANG_MAL;
-				queryTempl[0] = "";		// pre
-				queryTempl[1] = ";\n";		// post
-				queryTempl[2] = ";\n";		// separator
-				commandTempl[0] = "";		// pre
-				commandTempl[1] = "";		// post
+				queryTempl[0] = "";         // pre
+				queryTempl[1] = ";\n";      // post
+				queryTempl[2] = ";\n";      // separator
+				commandTempl[0] = "";       // pre
+				commandTempl[1] = "";       // post
 				break;
 			default:
 				lang = LANG_UNKNOWN;
@@ -1248,6 +1246,15 @@ public class MonetConnection
 		return isValid;
 	}
 
+	/**
+	 * Construct a Properties object holding all connection parameters such
+	 * as host, port, TLS configuration, autocommit, etc.
+	 * Passing this to {@link DriverManager.getConnection()} together
+	 * with the URL "jdbc:monetdb:" will create a new connection identical to
+	 * the current one.
+	 * @return
+	 */
+
 	public Properties getConnectionProperties() {
 		return target.getProperties();
 	}
@@ -3747,7 +3754,8 @@ public class MonetConnection
 		}
 	}
 
-	public static enum SqlOption {
+	/* encode knowledge of currently available handshake options as an enum. */
+	enum SqlOption {
 		Autocommit(1, "auto_commit"),
 		ReplySize(2, "reply_size"),
 		SizeHeader(3, "size_header"),
@@ -3766,6 +3774,7 @@ public class MonetConnection
 
 	private class SqlOptionsCallback extends MapiSocket.OptionsCallback {
 		private int level;
+
 		@Override
 		public void addOptions(String lang, int level) {
 			if (!lang.equals("sql"))
@@ -3784,7 +3793,7 @@ public class MonetConnection
 		}
 
 		private boolean contribute(SqlOption opt, int value) {
-			if (this.level <= opt.level)
+			if (opt.level >= this.level)
 				return false;
 			contribute(opt.field, value);
 			return true;
--- a/src/main/java/org/monetdb/jdbc/MonetDriver.java
+++ b/src/main/java/org/monetdb/jdbc/MonetDriver.java
@@ -8,19 +8,11 @@
 
 package org.monetdb.jdbc;
 
-import org.monetdb.mcl.net.MonetUrlParser;
-import org.monetdb.mcl.net.Parameter;
 import org.monetdb.mcl.net.Target;
 import org.monetdb.mcl.net.ValidationError;
 
 import java.net.URISyntaxException;
-import java.sql.Connection;
-import java.sql.Driver;
-import java.sql.DriverManager;
-import java.sql.DriverPropertyInfo;
-import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.sql.Types;
+import java.sql.*;
 import java.util.Map.Entry;
 import java.util.Properties;
 
@@ -70,11 +62,9 @@ public final class MonetDriver implement
 	 */
 	@Override
 	public boolean acceptsURL(final String url) {
-        if (url == null)
+		if (url == null)
 			return false;
-        if (url.startsWith("jdbc:monetdb:") || url.startsWith("jdbc:monetdbs:"))
-			return true;
-        return false;
+		return url.startsWith("jdbc:monetdb:") || url.startsWith("jdbc:monetdbs:");
 	}
 
 	/**
--- a/src/main/java/org/monetdb/mcl/net/MapiSocket.java
+++ b/src/main/java/org/monetdb/mcl/net/MapiSocket.java
@@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.*;
-import java.util.stream.Collectors;
 
 import org.monetdb.mcl.MCLException;
 import org.monetdb.mcl.io.BufferedMCLReader;
@@ -81,7 +80,12 @@ import org.monetdb.mcl.parser.MCLParseEx
  * @see org.monetdb.mcl.io.BufferedMCLWriter
  */
 public final class MapiSocket {
-	public static final byte[] NUL_BYTES = new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, };
+	/* an even number of NUL bytes used during the handshake */
+	private static final byte[] NUL_BYTES = new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, };
+
+	/* A mapping between hash algorithm names as used in the MAPI
+	 * protocol, and the names by which the Java runtime knows them.
+	 */
 	private static final String[][] KNOWN_ALGORITHMS = new String[][] {
 			{"SHA512", "SHA-512"},
 			{"SHA384", "SHA-384"},
@@ -262,6 +266,26 @@ public final class MapiSocket {
 		return connect(new Target(url, props), null);
 	}
 
+		/**
+	 * Connect according to the settings in the 'target' parameter.
+	 * If followRedirect is false, a RedirectionException is
+	 * thrown when a redirect is encountered.
+	 * 
+	 * Some settings, such as the initial reply size, can already be configured
+	 * during the handshake, saving a command round-trip later on.
+	 * To do so, create and pass a subclass of {@link MapiSocket.OptionsCallback}.
+	 * 
+	 * @param target the connection settings
+	 * @param callback will be called if the server allows options to be set during the
+	 * initial handshake
+	 * @return A List with informational (warning) messages. If this
+	 *		list is empty; then there are no warnings.
+	 * @throws IOException if an I/O error occurs when creating the socket
+	 * @throws SocketException - if there is an error in the underlying protocol, such as a TCP error.
+	 * @throws UnknownHostException if the IP address of the host could not be determined
+	 * @throws MCLParseException if bogus data is received
+	 * @throws MCLException if an MCL related error occurs
+	 */
 	public List<String> connect(Target target, OptionsCallback callback) throws MCLException, MCLParseException, IOException {
 		// get rid of any earlier connection state, including the existing target
 		close();
@@ -521,7 +545,7 @@ public final class MapiSocket {
             }
             return digest;
         }
-		String algoNames = algos.stream().collect(Collectors.joining());
+		String algoNames = String.join(",", algos);
 		throw new MCLException("No supported hash algorithm: " + algoNames);
 	}
 
@@ -1448,9 +1472,35 @@ public final class MapiSocket {
 		}
 	}
 
+	/**
+	 * Callback used during the initial MAPI handshake.
+	 * 
+	 * Newer MonetDB versions allow setting some options during the handshake.
+	 * The options are language-specific and each has a 'level'. The server
+	 * advertises up to which level options are supported for a given language
+	 * and for each language/option combination, {@link addOptions} will be invoked.
+	 * It should call {@link contribute} for each option it wants to set.
+	 * 
+	 * At the time of writing, only the 'sql' language supports options,
+	 * they are listed in enum mapi_handshake_options_levels in mapi.h.
+	 */
 	public static abstract class OptionsCallback {
 		private StringBuilder buffer;
 
+		/**
+		 * Callback called for each language/level combination supported by the
+		 * server. May call {@link contribute} for options with a level STRICTLY
+		 * LOWER than the level passed as a parameter.
+		 * @param lang language advertised by the server
+		 * @param level one higher than the maximum supported option
+		 */
+		public abstract void addOptions(String lang, int level);
+
+		/**
+		 * Pass option=value during the handshake
+		 * @param field
+		 * @param value
+		 */
 		protected void contribute(String field, int value) {
 			if (buffer.length() > 0)
 				buffer.append(',');
@@ -1459,7 +1509,6 @@ public final class MapiSocket {
 			buffer.append(value);
 		}
 
-		public abstract void addOptions(String lang, int level);
 
 		void setBuffer(StringBuilder buf) {
 			buffer = buf;
--- a/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java
+++ b/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java
@@ -6,270 +6,275 @@ import java.net.URISyntaxException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 
+/**
+ * Helper class to keep the URL parsing code separate from the rest of
+ * the {@link Target} class.
+ */
 public class MonetUrlParser {
-    private final Target target;
-    private final String urlText;
-    private final URI url;
+	private final Target target;
+	private final String urlText;
+	private final URI url;
 
-    public MonetUrlParser(Target target, String url) throws URISyntaxException {
-        this.target = target;
-        this.urlText = url;
-        // we want to accept monetdb:// but the Java URI parser rejects that.
-        switch (url) {
-            case "monetdb:-":
-            case "monetdbs:-":
-                throw new URISyntaxException(url, "invalid MonetDB URL");
-            case "monetdb://":
-            case "monetdbs://":
-                url += "-";
-                break;
-        }
-        this.url = new URI(url);
-    }
+	private MonetUrlParser(Target target, String url) throws URISyntaxException {
+		this.target = target;
+		this.urlText = url;
+		// we want to accept monetdb:// but the Java URI parser rejects that.
+		switch (url) {
+			case "monetdb:-":
+			case "monetdbs:-":
+				throw new URISyntaxException(url, "invalid MonetDB URL");
+			case "monetdb://":
+			case "monetdbs://":
+				url += "-";
+				break;
+		}
+		this.url = new URI(url);
+	}
 
-    public static void parse(Target target, String url) throws URISyntaxException, ValidationError {
-        if (url.equals("monetdb://")) {
-            // deal with peculiarity of Java's URI parser
-            url = "monetdb:///";
-        }
+	public static void parse(Target target, String url) throws URISyntaxException, ValidationError {
+		if (url.equals("monetdb://")) {
+			// deal with peculiarity of Java's URI parser
+			url = "monetdb:///";
+		}
 
-        target.barrier();
-        if (url.startsWith("mapi:")) {
-            try {
-                MonetUrlParser parser = new MonetUrlParser(target, url.substring(5));
-                parser.parseClassic();
-            } catch (URISyntaxException e) {
-                URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1);
-                exc.setStackTrace(e.getStackTrace());
-                throw exc;
-            }
-        } else {
-            MonetUrlParser parser = new MonetUrlParser(target, url);
-            parser.parseModern();
-        }
-        target.barrier();
-    }
+		target.barrier();
+		if (url.startsWith("mapi:")) {
+			try {
+				MonetUrlParser parser = new MonetUrlParser(target, url.substring(5));
+				parser.parseClassic();
+			} catch (URISyntaxException e) {
+				URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1);
+				exc.setStackTrace(e.getStackTrace());
+				throw exc;
+			}
+		} else {
+			MonetUrlParser parser = new MonetUrlParser(target, url);
+			parser.parseModern();
+		}
+		target.barrier();
+	}
 
-    public static String percentDecode(String context, String text) throws URISyntaxException {
-        try {
-            return URLDecoder.decode(text, "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e);
-        } catch (IllegalArgumentException e) {
-            throw new URISyntaxException(text, context + ": invalid percent escape");
-        }
-    }
+	public static String percentDecode(String context, String text) throws URISyntaxException {
+		try {
+			return URLDecoder.decode(text, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e);
+		} catch (IllegalArgumentException e) {
+			throw new URISyntaxException(text, context + ": invalid percent escape");
+		}
+	}
 
-    public static String percentEncode(String text) {
-        try {
-            return URLEncoder.encode(text, "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            throw new RuntimeException(e);
-        }
-    }
+	public static String percentEncode(String text) {
+		try {
+			return URLEncoder.encode(text, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
 
-    private void parseModern() throws URISyntaxException, ValidationError {
-        clearBasic();
+	private void parseModern() throws URISyntaxException, ValidationError {
+		clearBasic();
 
-        String scheme = url.getScheme();
-        if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
-        switch (scheme) {
-            case "monetdb":
-                target.setTls(false);
-                break;
-            case "monetdbs":
-                target.setTls(true);
-                break;
-            default:
-                throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
-        }
+		String scheme = url.getScheme();
+		if (scheme == null)
+			throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
+		switch (scheme) {
+			case "monetdb":
+				target.setTls(false);
+				break;
+			case "monetdbs":
+				target.setTls(true);
+				break;
+			default:
+				throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
+		}
 
-        // The built-in getHost and getPort methods do strange things
-        // in edge cases such as percent-encoded host names and
-        // invalid port numbers
-        String authority = url.getAuthority();
-        String host;
-        String remainder;
-        int pos;
-        if (authority == null) {
-            if (!url.getRawSchemeSpecificPart().startsWith("//")) {
-                throw new URISyntaxException(urlText, "expected //");
-            }
-            host = "";
-            remainder = "";
-        } else if (authority.equals("-")) {
-            host = "";
-            remainder = "";
-        } else {
-            if (authority.startsWith("[")) {
-                // IPv6
-                pos = authority.indexOf(']');
-                if (pos < 0)
-                    throw new URISyntaxException(urlText, "unmatched '['");
-                host = authority.substring(1, pos);
-                remainder = authority.substring(pos + 1);
-            } else if ((pos = authority.indexOf(':')) >= 0) {
-                host = authority.substring(0, pos);
-                remainder = authority.substring(pos);
-            } else {
-                host = authority;
-                remainder = "";
-            }
-        }
-        host = Target.unpackHost(host);
-        target.setHost(host);
+		// The built-in getHost and getPort methods do strange things
+		// in edge cases such as percent-encoded host names and
+		// invalid port numbers
+		String authority = url.getAuthority();
+		String host;
+		String remainder;
+		int pos;
+		if (authority == null) {
+			if (!url.getRawSchemeSpecificPart().startsWith("//")) {
+				throw new URISyntaxException(urlText, "expected //");
+			}
+			host = "";
+			remainder = "";
+		} else if (authority.equals("-")) {
+			host = "";
+			remainder = "";
+		} else {
+			if (authority.startsWith("[")) {
+				// IPv6
+				pos = authority.indexOf(']');
+				if (pos < 0)
+					throw new URISyntaxException(urlText, "unmatched '['");
+				host = authority.substring(1, pos);
+				remainder = authority.substring(pos + 1);
+			} else if ((pos = authority.indexOf(':')) >= 0) {
+				host = authority.substring(0, pos);
+				remainder = authority.substring(pos);
+			} else {
+				host = authority;
+				remainder = "";
+			}
+		}
+		host = Target.unpackHost(host);
+		target.setHost(host);
 
-        if (remainder.isEmpty()) {
-            // do nothing
-        } else if (remainder.startsWith(":")) {
-            String portStr = remainder.substring(1);
-            try {
-                int port = Integer.parseInt(portStr);
-                if (port <= 0 || port > 65535)
-                    portStr = null;
-            } catch (NumberFormatException e) {
-                portStr = null;
-            }
-            if (portStr == null)
-                throw new ValidationError(urlText, "invalid port number");
-            target.setString(Parameter.PORT, portStr);
-        }
+		if (remainder.isEmpty()) {
+			// do nothing
+		} else if (remainder.startsWith(":")) {
+			String portStr = remainder.substring(1);
+			try {
+				int port = Integer.parseInt(portStr);
+				if (port <= 0 || port > 65535)
+					portStr = null;
+			} catch (NumberFormatException e) {
+				portStr = null;
+			}
+			if (portStr == null)
+				throw new ValidationError(urlText, "invalid port number");
+			target.setString(Parameter.PORT, portStr);
+		}
 
-        String path = url.getRawPath();
-        String[] parts = path.split("/", 4);
-        // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist>
-        switch (parts.length) {
-            case 4:
-                target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
-                // fallthrough
-            case 3:
-                target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
-                // fallthrough
-            case 2:
-                target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
-            case 1:
-            case 0:
-                // fallthrough
-                break;
-        }
+		String path = url.getRawPath();
+		String[] parts = path.split("/", 4);
+		// <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist>
+		switch (parts.length) {
+			case 4:
+				target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
+				// fallthrough
+			case 3:
+				target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
+				// fallthrough
+			case 2:
+				target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
+			case 1:
+			case 0:
+				// fallthrough
+				break;
+		}
 
-        final String query = url.getRawQuery();
-        if (query != null) {
-            final String args[] = query.split("&");
-            for (int i = 0; i < args.length; i++) {
-                pos = args[i].indexOf('=');
-                if (pos <= 0) {
-                    throw new URISyntaxException(args[i], "invalid key=value pair");
-                }
-                String key = args[i].substring(0, pos);
-                key = percentDecode(key, key);
-                Parameter parm = Parameter.forName(key);
-                if (parm != null && parm.isCore)
-                    throw new URISyntaxException(key, key + "= is not allowed as a query parameter");
+		final String query = url.getRawQuery();
+		if (query != null) {
+			final String[] args = query.split("&");
+			for (int i = 0; i < args.length; i++) {
+				pos = args[i].indexOf('=');
+				if (pos <= 0) {
+					throw new URISyntaxException(args[i], "invalid key=value pair");
+				}
+				String key = args[i].substring(0, pos);
+				key = percentDecode(key, key);
+				Parameter parm = Parameter.forName(key);
+				if (parm != null && parm.isCore)
+					throw new URISyntaxException(key, key + "= is not allowed as a query parameter");
 
-                String value = args[i].substring(pos + 1);
-                target.setString(key, percentDecode(key, value));
-            }
-        }
-    }
+				String value = args[i].substring(pos + 1);
+				target.setString(key, percentDecode(key, value));
+			}
+		}
+	}
 
-    private void parseClassic() throws URISyntaxException, ValidationError {
-        if (!url.getRawSchemeSpecificPart().startsWith("//")) {
-            throw new URISyntaxException(urlText, "expected //");
-        }
+	private void parseClassic() throws URISyntaxException, ValidationError {
+		if (!url.getRawSchemeSpecificPart().startsWith("//")) {
+			throw new URISyntaxException(urlText, "expected //");
+		}
 
-        String scheme = url.getScheme();
-        if (scheme == null)
-            scheme = "";
-        switch (scheme) {
-            case "monetdb":
-                parseClassicAuthorityAndPath();
-                break;
-            case "merovingian":
-                String authority = url.getRawAuthority();
-                // authority must be "proxy" ignore authority and path
-                boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy");
-                if (!valid)
-                    throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported");
-                break;
-            default:
-                throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://");
-        }
+		String scheme = url.getScheme();
+		if (scheme == null)
+			scheme = "";
+		switch (scheme) {
+			case "monetdb":
+				parseClassicAuthorityAndPath();
+				break;
+			case "merovingian":
+				String authority = url.getRawAuthority();
+				// authority must be "proxy" ignore authority and path
+				boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy");
+				if (!valid)
+					throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported");
+				break;
+			default:
+				throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://");
+		}
 
-        final String query = url.getRawQuery();
-        if (query != null) {
-            final String args[] = query.split("&");
-            for (int i = 0; i < args.length; i++) {
-                String arg = args[i];
-                if (arg.startsWith("language=")) {
-                    String language = arg.substring(9);
-                    target.setString(Parameter.LANGUAGE, language);
-                } else if (arg.startsWith("database=")) {
-                    String database = arg.substring(9);
-                    target.setString(Parameter.DATABASE, database);
-                } else {
-                    // ignore
-                }
-            }
-        }
-    }
+		final String query = url.getRawQuery();
+		if (query != null) {
+			final String[] args = query.split("&");
+			for (int i = 0; i < args.length; i++) {
+				String arg = args[i];
+				if (arg.startsWith("language=")) {
+					String language = arg.substring(9);
+					target.setString(Parameter.LANGUAGE, language);
+				} else if (arg.startsWith("database=")) {
+					String database = arg.substring(9);
+					target.setString(Parameter.DATABASE, database);
+				} else {
+					// ignore
+				}
+			}
+		}
+	}
 
-    private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError {
-        clearBasic();
-        String authority = url.getRawAuthority();
-        String host;
-        String portStr;
-        int pos;
-        if (authority == null) {
-            host = "";
-            portStr = "";
-        } else if (authority.indexOf('@') >= 0) {
-            throw new URISyntaxException(urlText, "user@host syntax is not allowed");
-        } else if ((pos = authority.indexOf(':')) >= 0) {
-            host = authority.substring(0, pos);
-            portStr = authority.substring(pos + 1);
-        } else {
-            host = authority;
-            portStr = "";
-        }
+	private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError {
+		clearBasic();
+		String authority = url.getRawAuthority();
+		String host;
+		String portStr;
+		int pos;
+		if (authority == null) {
+			host = "";
+			portStr = "";
+		} else if (authority.indexOf('@') >= 0) {
+			throw new URISyntaxException(urlText, "user@host syntax is not allowed");
+		} else if ((pos = authority.indexOf(':')) >= 0) {
+			host = authority.substring(0, pos);
+			portStr = authority.substring(pos + 1);
+		} else {
+			host = authority;
+			portStr = "";
+		}
 
-        if (!portStr.isEmpty()) {
-            int port;
-            try {
-                port = Integer.parseInt(portStr);
-            } catch (NumberFormatException e) {
-                port = -1;
-            }
-            if (port <= 0) {
-                throw new ValidationError(urlText, "invalid port number");
-            }
-            target.setString(Parameter.PORT, portStr);
-        }
+		if (!portStr.isEmpty()) {
+			int port;
+			try {
+				port = Integer.parseInt(portStr);
+			} catch (NumberFormatException e) {
+				port = -1;
+			}
+			if (port <= 0) {
+				throw new ValidationError(urlText, "invalid port number");
+			}
+			target.setString(Parameter.PORT, portStr);
+		}
 
-        String path = url.getRawPath();
-        if (host.isEmpty() && portStr.isEmpty()) {
-            // socket
-            target.clear(Parameter.HOST);
-            target.setString(Parameter.SOCK, path != null ? path : "");
-        } else {
-            // tcp
-            target.clear(Parameter.SOCK);
-            target.setString(Parameter.HOST, host);
-            if (path == null || path.isEmpty()) {
-                // do nothing
-            } else if (!path.startsWith("/")) {
-                throw new URISyntaxException(urlText, "expect path to start with /");
-            } else {
-                String database = path.substring(1);
-                target.setString(Parameter.DATABASE, database);
-            }
-        }
-    }
+		String path = url.getRawPath();
+		if (host.isEmpty() && portStr.isEmpty()) {
+			// socket
+			target.clear(Parameter.HOST);
+			target.setString(Parameter.SOCK, path != null ? path : "");
+		} else {
+			// tcp
+			target.clear(Parameter.SOCK);
+			target.setString(Parameter.HOST, host);
+			if (path == null || path.isEmpty()) {
+				// do nothing
+			} else if (!path.startsWith("/")) {
+				throw new URISyntaxException(urlText, "expect path to start with /");
+			} else {
+				String database = path.substring(1);
+				target.setString(Parameter.DATABASE, database);
+			}
+		}
+	}
 
-    private void clearBasic() {
-        target.clear(Parameter.TLS);
-        target.clear(Parameter.HOST);
-        target.clear(Parameter.PORT);
-        target.clear(Parameter.DATABASE);
-    }
+	private void clearBasic() {
+		target.clear(Parameter.TLS);
+		target.clear(Parameter.HOST);
+		target.clear(Parameter.PORT);
+		target.clear(Parameter.DATABASE);
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/Parameter.java
+++ b/src/main/java/org/monetdb/mcl/net/Parameter.java
@@ -3,99 +3,142 @@ package org.monetdb.mcl.net;
 
 import java.util.Calendar;
 
+/**
+ * Enumerates things that can be configured on a connection to MonetDB.
+ */
 public enum Parameter {
-    TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true),
-    HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true),
-    PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true),
-    DATABASE("database", ParameterType.Str, "", "name of database to connect to", true),
-    TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true),
-    TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true),
-    SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false),
-    SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false),
-    CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false),
-    CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false),
-    CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false),
-    CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false),
-    USER("user", ParameterType.Str, "", "user name to authenticate as", false),
-    PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false),
-    LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false),
-    AUTOCOMMIT("autocommit", ParameterType.Bool, true, "initial value of autocommit", false),
-    SCHEMA("schema", ParameterType.Str, "", "initial schema", false),
-    TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false),
-    BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false),
-    REPLYSIZE("replysize", ParameterType.Int, 250, "rows beyond this limit are retrieved on demand, <1 means unlimited", false),
-    FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false),
-    HASH("hash", ParameterType.Str, "", "specific to jdbc", false),
-    DEBUG("debug", ParameterType.Bool, false, "specific to jdbc", false),
-    LOGFILE("logfile", ParameterType.Str, "", "specific to jdbc", false),
+	TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true),
+	HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true),
+	PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true),
+	DATABASE("database", ParameterType.Str, "", "name of database to connect to", true),
+	TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true),
+	TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true),
+	SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false),
+	SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false),
+	CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false),
+	CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false),
+	CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false),
+	CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false),
+	USER("user", ParameterType.Str, "", "user name to authenticate as", false),
+	PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false),
+	LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false),
+	AUTOCOMMIT("autocommit", ParameterType.Bool, true, "initial value of autocommit", false),
+	SCHEMA("schema", ParameterType.Str, "", "initial schema", false),
+	TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false),
+	BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false),
+	REPLYSIZE("replysize", ParameterType.Int, 250, "rows beyond this limit are retrieved on demand, <1 means unlimited", false),
+	FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false),
+	HASH("hash", ParameterType.Str, "", "specific to jdbc", false),
+	DEBUG("debug", ParameterType.Bool, false, "specific to jdbc", false),
+	LOGFILE("logfile", ParameterType.Str, "", "specific to jdbc", false),
 
-    SO_TIMEOUT("so_timeout", ParameterType.Int, 0, "abort if network I/O does not complete in this many milliseconds", false),
-    CLOB_AS_VARCHAR("treat_clob_as_varchar", ParameterType.Bool, true, "return CLOB/TEXT data as type VARCHAR instead of type CLOB", false),
-    BLOB_AS_BINARY("treat_blob_as_binary", ParameterType.Bool, true, "return BLOB data as type BINARY instead of type BLOB", false),
-    ;
+	SO_TIMEOUT("so_timeout", ParameterType.Int, 0, "abort if network I/O does not complete in this many milliseconds", false), CLOB_AS_VARCHAR("treat_clob_as_varchar", ParameterType.Bool, true, "return CLOB/TEXT data as type VARCHAR instead of type CLOB", false), BLOB_AS_BINARY("treat_blob_as_binary", ParameterType.Bool, true, "return BLOB data as type BINARY instead of type BLOB", false),
+	;
 
-    public final String name;
-    public final ParameterType type;
-    private final Object defaultValue;
-    public final String description;
-    public final boolean isCore;
+	public final String name;
+	public final ParameterType type;
+	public final String description;
+	public final boolean isCore;
+	private final Object defaultValue;
 
-    Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) {
-        this.name = name;
-        this.type = type;
-        this.isCore = isCore;
-        this.defaultValue = defaultValue;
-        this.description = description;
-    }
+	Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) {
+		this.name = name;
+		this.type = type;
+		this.isCore = isCore;
+		this.defaultValue = defaultValue;
+		this.description = description;
+	}
 
-    public static Parameter forName(String name) {
-        switch (name) {
-            case "tls": return TLS;
-            case "host": return HOST;
-            case "port": return PORT;
-            case "database": return DATABASE;
-            case "tableschema": return TABLESCHEMA;
-            case "table": return TABLE;
-            case "sock": return SOCK;
-            case "sockdir": return SOCKDIR;
-            case "cert": return CERT;
-            case "certhash": return CERTHASH;
-            case "clientkey": return CLIENTKEY;
-            case "clientcert": return CLIENTCERT;
-            case "user": return USER;
-            case "password": return PASSWORD;
-            case "language": return LANGUAGE;
-            case "autocommit": return AUTOCOMMIT;
-            case "schema": return SCHEMA;
-            case "timezone": return TIMEZONE;
-            case "binary": return BINARY;
-            case "replysize": return REPLYSIZE;
-            case "fetchsize": return FETCHSIZE;
-            case "hash": return HASH;
-            case "debug": return DEBUG;
-            case "logfile": return LOGFILE;
-            case "so_timeout": return SO_TIMEOUT;
-            case "treat_clob_as_varchar": return CLOB_AS_VARCHAR;
-            case "treat_blob_as_binary": return BLOB_AS_BINARY;
-            default: return null;
-        }
-    }
+	public static Parameter forName(String name) {
+		switch (name) {
+			case "tls":
+				return TLS;
+			case "host":
+				return HOST;
+			case "port":
+				return PORT;
+			case "database":
+				return DATABASE;
+			case "tableschema":
+				return TABLESCHEMA;
+			case "table":
+				return TABLE;
+			case "sock":
+				return SOCK;
+			case "sockdir":
+				return SOCKDIR;
+			case "cert":
+				return CERT;
+			case "certhash":
+				return CERTHASH;
+			case "clientkey":
+				return CLIENTKEY;
+			case "clientcert":
+				return CLIENTCERT;
+			case "user":
+				return USER;
+			case "password":
+				return PASSWORD;
+			case "language":
+				return LANGUAGE;
+			case "autocommit":
+				return AUTOCOMMIT;
+			case "schema":
+				return SCHEMA;
+			case "timezone":
+				return TIMEZONE;
+			case "binary":
+				return BINARY;
+			case "replysize":
+				return REPLYSIZE;
+			case "fetchsize":
+				return FETCHSIZE;
+			case "hash":
+				return HASH;
+			case "debug":
+				return DEBUG;
+			case "logfile":
+				return LOGFILE;
+			case "so_timeout":
+				return SO_TIMEOUT;
+			case "treat_clob_as_varchar":
+				return CLOB_AS_VARCHAR;
+			case "treat_blob_as_binary":
+				return BLOB_AS_BINARY;
+			default:
+				return null;
+		}
+	}
 
-    public static boolean isIgnored(String name) {
-        if (Parameter.forName(name) != null)
-            return false;
-        return name.contains("_");
-    }
+	/**
+	 * Determine if a given setting can safely be ignored.
+	 * The ground rule is that if we encounter an unknown setting
+	 * without an underscore in the name, it is an error. If it has
+	 * an underscore in its name, it can be ignored.
+	 * @param name the name of the setting to check
+	 * @return true if it can safely be ignored
+	 */
+	public static boolean isIgnored(String name) {
+		if (Parameter.forName(name) != null)
+			return false;
+		return name.contains("_");
+	}
 
-    public Object getDefault() {
-        switch (this) {
-            case TIMEZONE:
-                Calendar cal = Calendar.getInstance();
-                int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
-                int offsetSeconds = offsetMillis / 1000;
-                return (Integer)offsetSeconds;
-            default:
-                return defaultValue;
-        }
-    }
+	/**
+	 * Return a default value for the given setting, as an Object of the appropriate type.
+	 * Note that the value returned for TIMEZONE may change if the system time zone
+	 * is changed or if Daylight Saving Time starts or ends.
+	 * @return
+	 */
+	public Object getDefault() {
+		switch (this) {
+			case TIMEZONE:
+				Calendar cal = Calendar.getInstance();
+				int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
+				int offsetSeconds = offsetMillis / 1000;
+				return offsetSeconds;
+			default:
+				return defaultValue;
+		}
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/ParameterType.java
+++ b/src/main/java/org/monetdb/mcl/net/ParameterType.java
@@ -1,68 +1,103 @@
 package org.monetdb.mcl.net;
 
+/**
+ * Enumeration of the types a {@link Parameter} may have.
+ */
 public enum ParameterType {
-    Str,
-    Int,
-    Bool,
-    Path;
-
-    public Object parse(String name, String value) throws ValidationError {
-        if (value == null)
-            return null;
-
-        try {
-            switch (this) {
-                case Bool:
-                    return parseBool(value);
-                case Int:
-                    return Integer.parseInt(value);
-                case Str:
-                case Path:
-                    return value;
-                default:
-                    throw new IllegalStateException("unreachable");
-            }
-        } catch (IllegalArgumentException e) {
-            String message = e.toString();
-            throw new ValidationError(name, message);
-        }
-    }
+	/**
+	 * The Parameter is an arbitrary string
+	 */
+	Str,
+	/** The Parameter can be interpreted as an {@link Integer} */
+	Int,
+	/** The Parameter is a {@link Boolean} and can be
+	 * written "true", "false", "on", "off", "yes" or "no".
+	 * Uppercase letters are also accepted
+	 */
+	Bool,
+	/**
+	 * Functionally the same as {@link ParameterType.Str } but
+	 * indicates the value is to be interpreted as a path on the
+	 * client's file system.
+	 */
+	Path;
 
-    public String format(Object value) {
-        switch (this) {
-                case Bool:
-                    return (Boolean)value ? "true": "false";
-                case Int:
-                    return Integer.toString((Integer)value);
-                case Str:
-                case Path:
-                    return (String) value;
-                default:
-                    throw new IllegalStateException("unreachable");
-            }
-    }
+	/**
+	 * Convert a string to a boolean, accepting true/false/yes/no/on/off.
+	 * 
+	 * Uppercase is also accepted.
+	 * @param value text to be parsed
+	 * @return boolean interpretation of the text
+	 */
+	public static boolean parseBool(String value) {
+		boolean lowered = false;
+		String original = value;
+		while (true) {
+			switch (value) {
+				case "true":
+				case "yes":
+				case "on":
+					return true;
+				case "false":
+				case "no":
+				case "off":
+					return false;
+				default:
+					if (!lowered) {
+						value = value.toLowerCase();
+						lowered = true;
+						continue;
+					}
+					throw new IllegalArgumentException("invalid boolean value: " + original);
+			}
+		}
+	}
 
-    public static boolean parseBool(String value) {
-        boolean lowered = false;
-        String original = value;
-        while (true) {
-            switch (value) {
-                case "true":
-                case "yes":
-                case "on":
-                    return true;
-                case "false":
-                case "no":
-                case "off":
-                    return false;
-                default:
-                    if (!lowered) {
-                        value = value.toLowerCase();
-                        lowered = true;
-                        continue;
-                    }
-                    throw new IllegalArgumentException("invalid boolean value: " + original);
-            }
-        }
-    }
+	/**
+	 * Convert text into an Object of the appropriate type
+	 * @param name name of the setting for use in error messages
+	 * @param value text to be converted
+	 * @return Object representation of the text
+	 * @throws ValidationError if the text cannot be converted
+	 */
+	public Object parse(String name, String value) throws ValidationError {
+		if (value == null)
+			return null;
+
+		try {
+			switch (this) {
+				case Bool:
+					return parseBool(value);
+				case Int:
+					return Integer.parseInt(value);
+				case Str:
+				case Path:
+					return value;
+				default:
+					throw new IllegalStateException("unreachable");
+			}
+		} catch (IllegalArgumentException e) {
+			String message = e.toString();
+			throw new ValidationError(name, message);
+		}
+	}
+
+	/**
+	 * Represent the object as a string.
+	 * @param value, must be of the appropriate type
+	 * @return textual representation
+	 */
+	public String format(Object value) {
+		switch (this) {
+			case Bool:
+				return (Boolean) value ? "true" : "false";
+			case Int:
+				return Integer.toString((Integer) value);
+			case Str:
+			case Path:
+				return (String) value;
+			default:
+				throw new IllegalStateException("unreachable");
+		}
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/SecureSocket.java
+++ b/src/main/java/org/monetdb/mcl/net/SecureSocket.java
@@ -13,171 +13,172 @@ import java.security.cert.X509Certificat
 import java.util.Collections;
 
 public class SecureSocket {
-    private static final String[] ENABLED_PROTOCOLS = {"TLSv1.3"};
-    private static final String[] APPLICATION_PROTOCOLS = {"mapi/9"};
-
-    public static Socket wrap(Target.Validated validated, Socket inner) throws IOException {
-        Target.Verify verify = validated.connectVerify();
-        SSLSocketFactory socketFactory;
-        boolean checkName = true;
-        try {
-            switch (verify) {
-                case System:
-                    socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
-                    break;
-                case Cert:
-                    KeyStore keyStore = keyStoreForCert(validated.getCert());
-                    socketFactory = certBasedSocketFactory(keyStore);
-                    break;
-                case Hash:
-                    socketFactory = hashBasedSocketFactory(validated.connectCertHashDigits());
-                    checkName = false;
-                    break;
-                default:
-                    throw new RuntimeException("unreachable: unexpected verification strategy " + verify.name());
-            }
-            return wrapSocket(inner, validated, socketFactory, checkName);
-        } catch (CertificateException e) {
-            throw new SSLException(e.getMessage(), e);
-        }
-    }
+	private static final String[] ENABLED_PROTOCOLS = {"TLSv1.3"};
+	private static final String[] APPLICATION_PROTOCOLS = {"mapi/9"};
 
-    private static SSLSocket wrapSocket(Socket inner, Target.Validated validated, SSLSocketFactory socketFactory, boolean checkName) throws IOException {
-        SSLSocket sock = (SSLSocket) socketFactory.createSocket(inner, validated.connectTcp(), validated.connectPort(), true);
-        sock.setUseClientMode(true);
-        SSLParameters parameters = sock.getSSLParameters();
-
-        parameters.setProtocols(ENABLED_PROTOCOLS);
-
-        parameters.setServerNames(Collections.singletonList(new SNIHostName(validated.connectTcp())));
-
-        if (checkName) {
-            parameters.setEndpointIdentificationAlgorithm("HTTPS");
-        }
+	public static Socket wrap(Target.Validated validated, Socket inner) throws IOException {
+		Target.Verify verify = validated.connectVerify();
+		SSLSocketFactory socketFactory;
+		boolean checkName = true;
+		try {
+			switch (verify) {
+				case System:
+					socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+					break;
+				case Cert:
+					KeyStore keyStore = keyStoreForCert(validated.getCert());
+					socketFactory = certBasedSocketFactory(keyStore);
+					break;
+				case Hash:
+					socketFactory = hashBasedSocketFactory(validated.connectCertHashDigits());
+					checkName = false;
+					break;
+				default:
+					throw new RuntimeException("unreachable: unexpected verification strategy " + verify.name());
+			}
+			return wrapSocket(inner, validated, socketFactory, checkName);
+		} catch (CertificateException e) {
+			throw new SSLException(e.getMessage(), e);
+		}
+	}
 
-        // Unfortunately, SSLParameters.setApplicationProtocols is only available
-        // since language level 9 and currently we're on 8.
-        // Still call it if it happens to be available.
-        try {
-            Method setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class);
-            setApplicationProtocols.invoke(parameters, (Object) APPLICATION_PROTOCOLS);
-        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {}
+	private static SSLSocket wrapSocket(Socket inner, Target.Validated validated, SSLSocketFactory socketFactory, boolean checkName) throws IOException {
+		SSLSocket sock = (SSLSocket) socketFactory.createSocket(inner, validated.connectTcp(), validated.connectPort(), true);
+		sock.setUseClientMode(true);
+		SSLParameters parameters = sock.getSSLParameters();
+
+		parameters.setProtocols(ENABLED_PROTOCOLS);
+
+		parameters.setServerNames(Collections.singletonList(new SNIHostName(validated.connectTcp())));
+
+		if (checkName) {
+			parameters.setEndpointIdentificationAlgorithm("HTTPS");
+		}
 
-        sock.setSSLParameters(parameters);
-        sock.startHandshake();
-        return sock;
-    }
+		// Unfortunately, SSLParameters.setApplicationProtocols is only available
+		// since language level 9 and currently we're on 8.
+		// Still call it if it happens to be available.
+		try {
+			Method setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class);
+			setApplicationProtocols.invoke(parameters, (Object) APPLICATION_PROTOCOLS);
+		} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {
+		}
 
-    private static X509Certificate loadCertificate(String path) throws CertificateException, IOException {
-        CertificateFactory factory = CertificateFactory.getInstance("X509");
-        try (FileInputStream s = new FileInputStream(path)) {
-            return (X509Certificate) factory.generateCertificate(s);
-        }
-    }
+		sock.setSSLParameters(parameters);
+		sock.startHandshake();
+		return sock;
+	}
 
-    private static SSLSocketFactory certBasedSocketFactory(KeyStore store) throws IOException, CertificateException {
-        TrustManagerFactory trustManagerFactory;
-        try {
-            trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-            trustManagerFactory.init(store);
-        } catch (NoSuchAlgorithmException | KeyStoreException e) {
-            throw new RuntimeException("Could not create TrustManagerFactory", e);
-        }
+	private static X509Certificate loadCertificate(String path) throws CertificateException, IOException {
+		CertificateFactory factory = CertificateFactory.getInstance("X509");
+		try (FileInputStream s = new FileInputStream(path)) {
+			return (X509Certificate) factory.generateCertificate(s);
+		}
+	}
 
-        SSLContext context;
-        try {
-            context = SSLContext.getInstance("TLS");
-            context.init(null, trustManagerFactory.getTrustManagers(), null);
-        } catch (NoSuchAlgorithmException | KeyManagementException e) {
-            throw new RuntimeException("Could not create SSLContext", e);
-        }
-
-        return context.getSocketFactory();
-    }
+	private static SSLSocketFactory certBasedSocketFactory(KeyStore store) throws IOException, CertificateException {
+		TrustManagerFactory trustManagerFactory;
+		try {
+			trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+			trustManagerFactory.init(store);
+		} catch (NoSuchAlgorithmException | KeyStoreException e) {
+			throw new RuntimeException("Could not create TrustManagerFactory", e);
+		}
 
-    private static KeyStore keyStoreForCert(String path) throws IOException, CertificateException {
-        try {
-            X509Certificate cert = loadCertificate(path);
-            KeyStore store = emptyKeyStore();
-            store.setCertificateEntry("root", cert);
-            return store;
-        } catch (KeyStoreException e) {
-            throw new RuntimeException("Could not create KeyStore for certificate", e);
-        }
-    }
+		SSLContext context;
+		try {
+			context = SSLContext.getInstance("TLS");
+			context.init(null, trustManagerFactory.getTrustManagers(), null);
+		} catch (NoSuchAlgorithmException | KeyManagementException e) {
+			throw new RuntimeException("Could not create SSLContext", e);
+		}
+
+		return context.getSocketFactory();
+	}
 
-    private static KeyStore emptyKeyStore() throws IOException, CertificateException {
-        KeyStore store;
-        try {
-            store = KeyStore.getInstance("PKCS12");
-            store.load(null, null);
-            return store;
-        } catch (KeyStoreException | NoSuchAlgorithmException e) {
-            throw new RuntimeException("Could not create KeyStore for certificate", e);
-        }
-    }
+	private static KeyStore keyStoreForCert(String path) throws IOException, CertificateException {
+		try {
+			X509Certificate cert = loadCertificate(path);
+			KeyStore store = emptyKeyStore();
+			store.setCertificateEntry("root", cert);
+			return store;
+		} catch (KeyStoreException e) {
+			throw new RuntimeException("Could not create KeyStore for certificate", e);
+		}
+	}
 
-    private static SSLSocketFactory hashBasedSocketFactory(String hashDigits) {
-        TrustManager trustManager = new HashBasedTrustManager(hashDigits);
-        try {
-            SSLContext context = SSLContext.getInstance("TLS");
-            context.init(null, new TrustManager[]{ trustManager}, null);
-            return context.getSocketFactory();
-        } catch (NoSuchAlgorithmException | KeyManagementException e) {
-            throw new RuntimeException("Could not create SSLContext", e);
-        }
+	private static KeyStore emptyKeyStore() throws IOException, CertificateException {
+		KeyStore store;
+		try {
+			store = KeyStore.getInstance("PKCS12");
+			store.load(null, null);
+			return store;
+		} catch (KeyStoreException | NoSuchAlgorithmException e) {
+			throw new RuntimeException("Could not create KeyStore for certificate", e);
+		}
+	}
 
-    }
+	private static SSLSocketFactory hashBasedSocketFactory(String hashDigits) {
+		TrustManager trustManager = new HashBasedTrustManager(hashDigits);
+		try {
+			SSLContext context = SSLContext.getInstance("TLS");
+			context.init(null, new TrustManager[]{trustManager}, null);
+			return context.getSocketFactory();
+		} catch (NoSuchAlgorithmException | KeyManagementException e) {
+			throw new RuntimeException("Could not create SSLContext", e);
+		}
 
-    private static class HashBasedTrustManager implements X509TrustManager {
-        private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
-        private final String hashDigits;
+	}
 
-        public HashBasedTrustManager(String hashDigits) {
-            this.hashDigits = hashDigits;
-        }
+	private static class HashBasedTrustManager implements X509TrustManager {
+		private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
+		private final String hashDigits;
+
+		public HashBasedTrustManager(String hashDigits) {
+			this.hashDigits = hashDigits;
+		}
 
 
-        @Override
-        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
-            throw new RuntimeException("this TrustManager is only suitable for client side connections");
-        }
+		@Override
+		public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+			throw new RuntimeException("this TrustManager is only suitable for client side connections");
+		}
 
-        @Override
-        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
-            X509Certificate cert = x509Certificates[0];
-            byte[] certBytes = cert.getEncoded();
+		@Override
+		public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+			X509Certificate cert = x509Certificates[0];
+			byte[] certBytes = cert.getEncoded();
 
-            // for now it's always SHA256.
-            byte[] hashBytes;
-            try {
-                MessageDigest hasher = MessageDigest.getInstance("SHA-256");
-                hasher.update(certBytes);
-                hashBytes = hasher.digest();
-            } catch (NoSuchAlgorithmException e) {
-                throw new RuntimeException("failed to instantiate hash digest");
-            }
+			// for now it's always SHA256.
+			byte[] hashBytes;
+			try {
+				MessageDigest hasher = MessageDigest.getInstance("SHA-256");
+				hasher.update(certBytes);
+				hashBytes = hasher.digest();
+			} catch (NoSuchAlgorithmException e) {
+				throw new RuntimeException("failed to instantiate hash digest");
+			}
 
-            // convert to hex digits
-            StringBuilder buffer = new StringBuilder(2 * hashBytes.length);
-            for (byte b: hashBytes) {
-                int hi = (b & 0xF0) >> 4;
-                int lo = b & 0x0F;
-                buffer.append(HEXDIGITS[hi]);
-                buffer.append(HEXDIGITS[lo]);
-            }
-            String certDigits = buffer.toString();
+			// convert to hex digits
+			StringBuilder buffer = new StringBuilder(2 * hashBytes.length);
+			for (byte b : hashBytes) {
+				int hi = (b & 0xF0) >> 4;
+				int lo = b & 0x0F;
+				buffer.append(HEXDIGITS[hi]);
+				buffer.append(HEXDIGITS[lo]);
+			}
+			String certDigits = buffer.toString();
 
-            if (!certDigits.startsWith(hashDigits)) {
-                throw new CertificateException("Certificate hash does not start with '" + hashDigits + "': " + certDigits);
-            }
+			if (!certDigits.startsWith(hashDigits)) {
+				throw new CertificateException("Certificate hash does not start with '" + hashDigits + "': " + certDigits);
+			}
 
 
-        }
+		}
 
-        @Override
-        public X509Certificate[] getAcceptedIssuers() {
-            return new X509Certificate[0];
-        }
-    }
+		@Override
+		public X509Certificate[] getAcceptedIssuers() {
+			return new X509Certificate[0];
+		}
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/Target.java
+++ b/src/main/java/org/monetdb/mcl/net/Target.java
@@ -5,731 +5,820 @@ import java.util.Properties;
 import java.util.regex.Pattern;
 
 public class Target {
-    private boolean tls = false;
-    private String host = "";
-    private int port = -1;
-    private String database = "";
-    private String tableschema = "";
-    private String table = "";
-    private String sock = "";
-    private String sockdir = "/tmp";
-    private String cert = "";
-    private String certhash = "";
-    private String clientkey = "";
-    private String clientcert = "";
-    private String user = "";
-    private String password = "";
-    private String language = "sql";
-    private boolean autocommit = true;
-    private String schema = "";
-    private int timezone;
-    private String binary = "on";
-    private int replySize = 250;
-    private String hash = "";
-    private boolean debug = false;
-    private String logfile = "";
-    private int soTimeout = 0;
-    private boolean treatClobAsVarchar = true;
-    private boolean treatBlobAsBinary = true;
+	protected static final Target defaults = new Target();
+	private static final Pattern namePattern = Pattern.compile("^[a-zzA-Z_][-a-zA-Z0-9_.]*$");
+	private static final Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$");
+	private boolean tls = false;
+	private String host = "";
+	private int port = -1;
+	private String database = "";
+	private String tableschema = "";
+	private String table = "";
+	private String sock = "";
+	private String sockdir = "/tmp";
+	private String cert = "";
+	private String certhash = "";
+	private String clientkey = "";
+	private String clientcert = "";
+	private String user = "";
+	private String password = "";
+	private String language = "sql";
+	private boolean autocommit = true;
+	private String schema = "";
+	private int timezone;
+	private String binary = "on";
+	private int replySize = 250;
+	private String hash = "";
+	private boolean debug = false;
+	private String logfile = "";
+	private int soTimeout = 0;
+	private boolean treatClobAsVarchar = true;
+	private boolean treatBlobAsBinary = true;
+	private boolean userWasSet = false;
+	private boolean passwordWasSet = false;
+	private Validated validated = null;
 
-    private boolean userWasSet = false;
-    private boolean passwordWasSet = false;
-    protected static final Target defaults = new Target();
-    private Validated validated = null;
-
-    private static Pattern namePattern = Pattern.compile("^[a-zzA-Z_][-a-zA-Z0-9_.]*$");
-    private static Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$");
+	public Target() {
+		this.timezone = (int) Parameter.TIMEZONE.getDefault();
+	}
 
-    public Target() {
-        this.timezone = (int)Parameter.TIMEZONE.getDefault();
-    }
-
-    public Target(String url, Properties props) throws URISyntaxException, ValidationError {
-        this();
-        setProperties(props);
-        parseUrl(url);
-    }
+	public Target(String url, Properties props) throws URISyntaxException, ValidationError {
+		this();
+		setProperties(props);
+		parseUrl(url);
+	}
 
-    public void barrier() {
-        if (userWasSet && !passwordWasSet)
-            password = "";
-        userWasSet = false;
-        passwordWasSet = false;
-    }
+	public static String packHost(String host) {
+		switch (host) {
+			case "localhost":
+				return "localhost.";
+			case "":
+				return "localhost";
+			default:
+				return host;
+		}
+	}
 
-    public static String packHost(String host) {
-        switch (host) {
-            case "localhost":
-                return "localhost.";
-            case "":
-                return "localhost";
-            default:
-                return host;
-        }
-    }
+	public static String unpackHost(String host) {
+		switch (host) {
+			case "localhost.":
+				return "localhost";
+			case "localhost":
+				return "";
+			default:
+				return host;
+		}
+	}
+
+	public void barrier() {
+		if (userWasSet && !passwordWasSet)
+			password = "";
+		userWasSet = false;
+		passwordWasSet = false;
+	}
 
-    public static String unpackHost(String host) {
-        switch (host) {
-            case "localhost.":
-                return "localhost";
-            case "localhost":
-                return "";
-            default:
-                return host;
-        }
-    }
+	public void setString(String key, String value) throws ValidationError {
+		Parameter parm = Parameter.forName(key);
+		if (parm != null)
+			setString(parm, value);
+		else if (!Parameter.isIgnored(key))
+			throw new ValidationError(key, "unknown parameter");
+	}
+
+	public void setString(Parameter parm, String value) throws ValidationError {
+		if (value == null)
+			throw new NullPointerException("'value' must not be null");
+		assign(parm, parm.type.parse(parm.name, value));
+	}
 
-    public void setString(String key, String value) throws ValidationError {
-        Parameter parm = Parameter.forName(key);
-        if (parm != null)
-            setString(parm, value);
-        else if (!Parameter.isIgnored(key))
-            throw new ValidationError(key, "unknown parameter");
-    }
+	public void clear(Parameter parm) {
+		assign(parm, parm.getDefault());
+	}
 
-    public void setString(Parameter parm, String value) throws ValidationError {
-        if (value == null)
-            throw new NullPointerException("'value' must not be null");
-        assign(parm, parm.type.parse(parm.name, value));
-    }
-
-    public void clear(Parameter parm) {
-        assign(parm, parm.getDefault());
-    }
+	public void parseUrl(String url) throws URISyntaxException, ValidationError {
+		if (url == null)
+			return;
+		if (url.startsWith("jdbc:"))
+			url = url.substring(5);
+		if (url.equals("monetdb:")) {
+			return;
+		}
+		MonetUrlParser.parse(this, url);
+	}
 
-    public void setProperties(Properties props) throws ValidationError {
-        if (props != null) {
-            for (String key : props.stringPropertyNames()) {
-                String value = props.getProperty(key);
-                if (key.equals(Parameter.HOST.name))
-                    value = Target.unpackHost(value);
-                setString(key, value);
-            }
-        }
-    }
-
-    public void parseUrl(String url) throws URISyntaxException, ValidationError {
-        if (url == null)
-            return;
-        if (url.startsWith("jdbc:"))
-            url = url.substring(5);
-        if (url.equals("monetdb:")) {
-            return;
-        }
-        MonetUrlParser.parse(this, url);
-    }
+	private void assign(Parameter parm, Object value) {
+		switch (parm) {
+			case TLS:
+				setTls((boolean) value);
+				break;
+			case HOST:
+				setHost((String) value);
+				break;
+			case PORT:
+				setPort((int) value);
+				break;
+			case DATABASE:
+				setDatabase((String) value);
+				break;
+			case TABLESCHEMA:
+				setTableschema((String) value);
+				break;
+			case TABLE:
+				setTable((String) value);
+				break;
+			case SOCK:
+				setSock((String) value);
+				break;
+			case SOCKDIR:
+				setSockdir((String) value);
+				break;
+			case CERT:
+				setCert((String) value);
+				break;
+			case CERTHASH:
+				setCerthash((String) value);
+				break;
+			case CLIENTKEY:
+				setClientkey((String) value);
+				break;
+			case CLIENTCERT:
+				setClientcert((String) value);
+				break;
+			case USER:
+				setUser((String) value);
+				break;
+			case PASSWORD:
+				setPassword((String) value);
+				break;
+			case LANGUAGE:
+				setLanguage((String) value);
+				break;
+			case AUTOCOMMIT:
+				setAutocommit((boolean) value);
+				break;
+			case SCHEMA:
+				setSchema((String) value);
+				break;
+			case TIMEZONE:
+				setTimezone((int) value);
+				break;
+			case BINARY:
+				setBinary((String) value);
+				break;
+			case REPLYSIZE:
+				setReplySize((int) value);
+				break;
+			case FETCHSIZE:
+				setReplySize((int) value);
+				break;
+			case HASH:
+				setHash((String) value);
+				break;
+			case DEBUG:
+				setDebug((boolean) value);
+				break;
+			case LOGFILE:
+				setLogfile((String) value);
+				break;
 
-    private void assign(Parameter parm, Object value) {
-        switch (parm) {
-            case TLS: setTls((boolean)value); break;
-            case HOST: setHost((String)value); break;
-            case PORT: setPort((int)value); break;
-            case DATABASE: setDatabase((String)value); break;
-            case TABLESCHEMA: setTableschema((String)value); break;
-            case TABLE: setTable((String)value); break;
-            case SOCK: setSock((String)value); break;
-            case SOCKDIR: setSockdir((String)value); break;
-            case CERT: setCert((String)value); break;
-            case CERTHASH: setCerthash((String)value); break;
-            case CLIENTKEY: setClientkey((String)value); break;
-            case CLIENTCERT: setClientcert((String)value); break;
-            case USER: setUser((String)value); break;
-            case PASSWORD: setPassword((String)value); break;
-            case LANGUAGE: setLanguage((String)value); break;
-            case AUTOCOMMIT: setAutocommit((boolean)value); break;
-            case SCHEMA: setSchema((String)value); break;
-            case TIMEZONE: setTimezone((int)value); break;
-            case BINARY: setBinary((String)value); break;
-            case REPLYSIZE: setReplySize((int)value); break;
-            case FETCHSIZE: setReplySize((int)value); break;
-            case HASH: setHash((String)value); break;
-            case DEBUG: setDebug((boolean)value); break;
-            case LOGFILE: setLogfile((String)value); break;
+			case SO_TIMEOUT:
+				setSoTimeout((int) value);
+				break;
+			case CLOB_AS_VARCHAR:
+				setTreatClobAsVarchar((boolean) value);
+				break;
+			case BLOB_AS_BINARY:
+				setTreatBlobAsBinary((boolean) value);
+				break;
 
-            case SO_TIMEOUT: setSoTimeout((int)value); break;
-            case CLOB_AS_VARCHAR: setTreatClobAsVarchar((boolean)value); break;
-            case BLOB_AS_BINARY: setTreatBlobAsBinary((boolean)value); break;
+			default:
+				throw new IllegalStateException("unreachable -- missing case: " + parm.name);
+		}
+	}
 
-            default:
-                throw new IllegalStateException("unreachable -- missing case: " + parm.name);
-        }
-    }
-
-    public String getString(Parameter parm) {
-        Object value = getObject(parm);
-        return parm.type.format(value);
-    }
+	public String getString(Parameter parm) {
+		Object value = getObject(parm);
+		return parm.type.format(value);
+	}
 
-    public Object getObject(Parameter parm) {
-        switch (parm) {
-            case TLS: return tls;
-            case HOST: return host;
-            case PORT: return port;
-            case DATABASE: return database;
-            case TABLESCHEMA: return tableschema;
-            case TABLE: return table;
-            case SOCK: return sock;
-            case SOCKDIR: return sockdir;
-            case CERT: return cert;
-            case CERTHASH: return certhash;
-            case CLIENTKEY: return clientkey;
-            case CLIENTCERT: return clientcert;
-            case USER: return user;
-            case PASSWORD: return password;
-            case LANGUAGE: return language;
-            case AUTOCOMMIT: return autocommit;
-            case SCHEMA: return schema;
-            case TIMEZONE: return timezone;
-            case BINARY: return binary;
-            case REPLYSIZE: return replySize;
-            case FETCHSIZE: return replySize;
-            case HASH: return hash;
-            case DEBUG: return debug;
-            case LOGFILE: return logfile;
-            case SO_TIMEOUT: return soTimeout;
-            case CLOB_AS_VARCHAR: return treatClobAsVarchar;
-            case BLOB_AS_BINARY: return treatBlobAsBinary;
-            default:
-                throw new IllegalStateException("unreachable -- missing case");
-        }
-    }
+	public Object getObject(Parameter parm) {
+		switch (parm) {
+			case TLS:
+				return tls;
+			case HOST:
+				return host;
+			case PORT:
+				return port;
+			case DATABASE:
+				return database;
+			case TABLESCHEMA:
+				return tableschema;
+			case TABLE:
+				return table;
+			case SOCK:
+				return sock;
+			case SOCKDIR:
+				return sockdir;
+			case CERT:
+				return cert;
+			case CERTHASH:
+				return certhash;
+			case CLIENTKEY:
+				return clientkey;
+			case CLIENTCERT:
+				return clientcert;
+			case USER:
+				return user;
+			case PASSWORD:
+				return password;
+			case LANGUAGE:
+				return language;
+			case AUTOCOMMIT:
+				return autocommit;
+			case SCHEMA:
+				return schema;
+			case TIMEZONE:
+				return timezone;
+			case BINARY:
+				return binary;
+			case REPLYSIZE:
+				return replySize;
+			case FETCHSIZE:
+				return replySize;
+			case HASH:
+				return hash;
+			case DEBUG:
+				return debug;
+			case LOGFILE:
+				return logfile;
+			case SO_TIMEOUT:
+				return soTimeout;
+			case CLOB_AS_VARCHAR:
+				return treatClobAsVarchar;
+			case BLOB_AS_BINARY:
+				return treatBlobAsBinary;
+			default:
+				throw new IllegalStateException("unreachable -- missing case");
+		}
+	}
 
-    public boolean isTls() {
-        return tls;
-    }
+	public boolean isTls() {
+		return tls;
+	}
+
+	public void setTls(boolean tls) {
+		this.tls = tls;
+		validated = null;
+	}
+
+	public String getHost() {
+		return host;
+	}
 
-    public void setTls(boolean tls) {
-        this.tls = tls;
-        validated = null;
-    }
+	public void setHost(String host) {
+		this.host = host;
+		validated = null;
+	}
+
+	public int getPort() {
+		return port;
+	}
+
+	public void setPort(int port) {
+		this.port = port;
+		validated = null;
+	}
+
+	public String getDatabase() {
+		return database;
+	}
 
-    public String getHost() {
-        return host;
-    }
+	public void setDatabase(String database) {
+		this.database = database;
+		validated = null;
+	}
+
+	public String getTableschema() {
+		return tableschema;
+	}
 
-    public void setHost(String host) {
-        this.host = host;
-        validated = null;
-    }
+	public void setTableschema(String tableschema) {
+		this.tableschema = tableschema;
+		validated = null;
+	}
+
+	public String getTable() {
+		return table;
+	}
 
-    public int getPort() {
-        return port;
-    }
+	public void setTable(String table) {
+		this.table = table;
+		validated = null;
+	}
+
+	public String getSock() {
+		return sock;
+	}
 
-    public void setPort(int port) {
-        this.port = port;
-        validated = null;
-    }
+	public void setSock(String sock) {
+		this.sock = sock;
+		validated = null;
+	}
 
-    public String getDatabase() {
-        return database;
-    }
+	public String getSockdir() {
+		return sockdir;
+	}
 
-    public void setDatabase(String database) {
-        this.database = database;
-        validated = null;
-    }
+	public void setSockdir(String sockdir) {
+		this.sockdir = sockdir;
+		validated = null;
+	}
+
+	public String getCert() {
+		return cert;
+	}
 
-    public String getTableschema() {
-        return tableschema;
-    }
+	public void setCert(String cert) {
+		this.cert = cert;
+		validated = null;
+	}
+
+	public String getCerthash() {
+		return certhash;
+	}
 
-    public void setTableschema(String tableschema) {
-        this.tableschema = tableschema;
-        validated = null;
-    }
+	public void setCerthash(String certhash) {
+		this.certhash = certhash;
+		validated = null;
+	}
 
-    public String getTable() {
-        return table;
-    }
+	public String getClientkey() {
+		return clientkey;
+	}
 
-    public void setTable(String table) {
-        this.table = table;
-        validated = null;
-    }
+	public void setClientkey(String clientkey) {
+		this.clientkey = clientkey;
+		validated = null;
+	}
 
-    public String getSock() {
-        return sock;
-    }
+	public String getClientcert() {
+		return clientcert;
+	}
 
-    public void setSock(String sock) {
-        this.sock = sock;
-        validated = null;
-    }
+	public void setClientcert(String clientcert) {
+		this.clientcert = clientcert;
+		validated = null;
+	}
 
-    public String getSockdir() {
-        return sockdir;
-    }
+	public String getUser() {
+		return user;
+	}
+
+	public void setUser(String user) {
+		this.user = user;
+		this.userWasSet = true;
+		validated = null;
+	}
 
-    public void setSockdir(String sockdir) {
-        this.sockdir = sockdir;
-        validated = null;
-    }
+	public String getPassword() {
+		return password;
+	}
 
-    public String getCert() {
-        return cert;
-    }
+	public void setPassword(String password) {
+		this.password = password;
+		this.passwordWasSet = true;
+		validated = null;
+	}
 
-    public void setCert(String cert) {
-        this.cert = cert;
-        validated = null;
-    }
+	public String getLanguage() {
+		return language;
+	}
+
+	public void setLanguage(String language) {
+		this.language = language;
+		validated = null;
+	}
 
-    public String getCerthash() {
-        return certhash;
-    }
+	public boolean isAutocommit() {
+		return autocommit;
+	}
+
+	public void setAutocommit(boolean autocommit) {
+		this.autocommit = autocommit;
+		validated = null;
+	}
 
-    public void setCerthash(String certhash) {
-        this.certhash = certhash;
-        validated = null;
-    }
+	public String getSchema() {
+		return schema;
+	}
 
-    public String getClientkey() {
-        return clientkey;
-    }
+	public void setSchema(String schema) {
+		this.schema = schema;
+		validated = null;
+	}
 
-    public void setClientkey(String clientkey) {
-        this.clientkey = clientkey;
-        validated = null;
-    }
+	public int getTimezone() {
+		return timezone;
+	}
+
+	public void setTimezone(int timezone) {
+		this.timezone = timezone;
+		validated = null;
+	}
 
-    public String getClientcert() {
-        return clientcert;
-    }
+	public String getBinary() {
+		return binary;
+	}
 
-    public void setClientcert(String clientcert) {
-        this.clientcert = clientcert;
-        validated = null;
-    }
+	public void setBinary(String binary) {
+		this.binary = binary;
+		validated = null;
+	}
+
+	public int getReplySize() {
+		return replySize;
+	}
 
-    public String getUser() {
-        return user;
-    }
+	public void setReplySize(int replySize) {
+		this.replySize = replySize;
+		validated = null;
+	}
 
-    public void setUser(String user) {
-        this.user = user;
-        this.userWasSet = true;
-        validated = null;
-    }
+	public String getHash() {
+		return hash;
+	}
+
+	public void setHash(String hash) {
+		this.hash = hash;
+		validated = null;
+	}
 
-    public String getPassword() {
-        return password;
-    }
+	public boolean isDebug() {
+		return debug;
+	}
+
+	public void setDebug(boolean debug) {
+		this.debug = debug;
+		validated = null;
+	}
 
-    public void setPassword(String password) {
-        this.password = password;
-        this.passwordWasSet = true;
-        validated = null;
-    }
+	public String getLogfile() {
+		return logfile;
+	}
 
-    public String getLanguage() {
-        return language;
-    }
+	public void setLogfile(String logfile) {
+		this.logfile = logfile;
+		validated = null;
+	}
 
-    public void setLanguage(String language) {
-        this.language = language;
-        validated = null;
-    }
+	public int getSoTimeout() {
+		return soTimeout;
+	}
 
-    public boolean isAutocommit() {
-        return autocommit;
-    }
+	public void setSoTimeout(int soTimeout) {
+		this.soTimeout = soTimeout;
+		validated = null;
+	}
+
+	public boolean isTreatClobAsVarchar() {
+		return treatClobAsVarchar;
+	}
 
-    public void setAutocommit(boolean autocommit) {
-        this.autocommit = autocommit;
-        validated = null;
-    }
+	public void setTreatClobAsVarchar(boolean treatClobAsVarchar) {
+		this.treatClobAsVarchar = treatClobAsVarchar;
+		validated = null;
+	}
 
-    public String getSchema() {
-        return schema;
-    }
+	public boolean isTreatBlobAsBinary() {
+		return treatBlobAsBinary;
+	}
 
-    public void setSchema(String schema) {
-        this.schema = schema;
-        validated = null;
-    }
+	public void setTreatBlobAsBinary(boolean treatBlobAsBinary) {
+		this.treatBlobAsBinary = treatBlobAsBinary;
+		validated = null;
+	}
 
-    public int getTimezone() {
-        return timezone;
-    }
+	public Validated validate() throws ValidationError {
+		if (validated == null)
+			validated = new Validated();
+		return validated;
+	}
 
-    public void setTimezone(int timezone) {
-        this.timezone = timezone;
-        validated = null;
-    }
-
-    public String getBinary() {
-        return binary;
-    }
+	public String buildUrl() {
+		final StringBuilder sb = new StringBuilder(128);
+		sb.append("jdbc:");
+		sb.append(tls ? "monetdbs" : "monetdb");
+		sb.append("://");
+		sb.append(packHost(host));
+		if (!Parameter.PORT.getDefault().equals(port)) {
+			sb.append(':');
+			sb.append(port);
+		}
+		sb.append('/').append(database);
+		String sep = "?";
+		for (Parameter parm : Parameter.values()) {
+			if (parm.isCore || parm == Parameter.USER || parm == Parameter.PASSWORD)
+				continue;
+			Object defaultValue = parm.getDefault();
+			if (defaultValue == null)
+				continue;
+			Object value = getObject(parm);
+			if (value.equals(defaultValue))
+				continue;
+			sb.append(sep).append(parm.name).append('=');
+			String raw = getString(parm);
+			String encoded = MonetUrlParser.percentEncode(raw);
+			sb.append(encoded);
+			sep = "&";
+		}
+		return sb.toString();
+	}
 
-    public void setBinary(String binary) {
-        this.binary = binary;
-        validated = null;
-    }
-
-    public int getReplySize() {
-        return replySize;
-    }
-
-    public void setReplySize(int replySize) {
-        this.replySize = replySize;
-        validated = null;
-    }
-
-    public String getHash() {
-        return hash;
-    }
+	public Properties getProperties() {
+		Properties props = new Properties();
+		for (Parameter parm : Parameter.values()) {
+			Object defaultValue = parm.getDefault();
+			if (defaultValue == null || defaultValue.equals(getObject(parm)))
+				continue;
+			String value = getString(parm);
+			if (parm == Parameter.HOST)
+				value = packHost(host);
+			props.setProperty(parm.name, value);
+		}
 
-    public void setHash(String hash) {
-        this.hash = hash;
-        validated = null;
-    }
+		return props;
+	}
 
-    public boolean isDebug() {
-        return debug;
-    }
+	public void setProperties(Properties props) throws ValidationError {
+		if (props != null) {
+			for (String key : props.stringPropertyNames()) {
+				String value = props.getProperty(key);
+				if (key.equals(Parameter.HOST.name))
+					value = Target.unpackHost(value);
+				setString(key, value);
+			}
+		}
+	}
+
+	public enum Verify {
+		None, Cert, Hash, System
+	}
+
+	public class Validated {
 
-    public void setDebug(boolean debug) {
-        this.debug = debug;
-        validated = null;
-    }
+		private final int nbinary;
 
-    public String getLogfile() {
-        return logfile;
-    }
+		Validated() throws ValidationError {
+
+			// 1. The parameters have the types listed in the table in [Section
+			//    Parameters](#parameters).
 
-    public void setLogfile(String logfile) {
-        this.logfile = logfile;
-        validated = null;
-    }
-
-    public int getSoTimeout() {
-        return soTimeout;
-    }
+			String binaryString = binary;
+			int binaryInt;
+			try {
+				binaryInt = (int) ParameterType.Int.parse(Parameter.BINARY.name, binaryString);
+			} catch (ValidationError e) {
+				try {
+					boolean b = (boolean) ParameterType.Bool.parse(Parameter.BINARY.name, binaryString);
+					binaryInt = b ? 65535 : 0;
+				} catch (ValidationError ee) {
+					throw new ValidationError("binary= must be either a number or true/yes/on/false/no/off");
+				}
+			}
+			if (binaryInt < 0)
+				throw new ValidationError("binary= cannot be negative");
+			nbinary = binaryInt;
 
 
-    public void setSoTimeout(int soTimeout) {
-        this.soTimeout = soTimeout;
-        validated = null;
-    }
-
-    public void setTreatClobAsVarchar(boolean treatClobAsVarchar) {
-        this.treatClobAsVarchar = treatClobAsVarchar;
-        validated = null;
-    }
-
-    public boolean isTreatClobAsVarchar() {
-        return treatClobAsVarchar;
-    }
-
-    public boolean isTreatBlobAsBinary() {
-        return treatBlobAsBinary;
-    }
-
-    public void setTreatBlobAsBinary(boolean treatBlobAsBinary) {
-        this.treatBlobAsBinary = treatBlobAsBinary;
-        validated = null;
-    }
-
-    public Validated validate() throws ValidationError {
-        if (validated == null)
-            validated = new Validated();
-        return validated;
-    }
+			// 2. At least one of **sock** and **host** must be empty.
+			if (!sock.isEmpty() && !host.isEmpty())
+				throw new ValidationError("sock=" + sock + " cannot be combined with host=" + host);
 
-    public String buildUrl() {
-        final StringBuilder sb = new StringBuilder(128);
-        sb.append("jdbc:");
-        sb.append(tls ? "monetdbs": "monetdb");
-        sb.append("://");
-        sb.append(packHost(host));
-        if (!Parameter.PORT.getDefault().equals(port)) {
-            sb.append(':');
-            sb.append(port);
-        }
-        sb.append('/').append(database);
-        String sep = "?";
-        for (Parameter parm: Parameter.values()) {
-            if (parm.isCore || parm == Parameter.USER || parm == Parameter.PASSWORD)
-                continue;
-            Object defaultValue = parm.getDefault();
-            if (defaultValue == null)
-                continue;
-            Object value = getObject(parm);
-            if (value.equals(defaultValue))
-                continue;
-            sb.append(sep).append(parm.name).append('=');
-            String raw = getString(parm);
-            String encoded = MonetUrlParser.percentEncode(raw);
-            sb.append(encoded);
-            sep = "&";
-        }
-        return sb.toString();
-    }
+			// 3. The string parameter **binary** must either parse as a boolean or as a
+			//    non-negative integer.
+			//
+			// (checked above)
 
-    public Properties getProperties() {
-        Properties props = new Properties();
-        for (Parameter parm: Parameter.values()) {
-            Object defaultValue = parm.getDefault();
-            if (defaultValue == null || defaultValue.equals(getObject(parm)))
-                continue;
-            String value = getString(parm);
-            if (parm == Parameter.HOST)
-                value = packHost(host);
-            props.setProperty(parm.name, value);
-        }
+			// 4. If **sock** is not empty, **tls** must be 'off'.
+			if (!sock.isEmpty() && tls)
+				throw new ValidationError("monetdbs:// cannot be combined with sock=");
 
-        return props;
-    }
-
-    public class Validated {
-
-        private final int nbinary;
-
-        Validated() throws ValidationError {
-
-            // 1. The parameters have the types listed in the table in [Section
-            //    Parameters](#parameters).
+			// 5. If **certhash** is not empty, it must be of the form `{sha256}hexdigits`
+			//    where hexdigits is a non-empty sequence of 0-9, a-f, A-F and colons.
+			// TODO
+			if (!certhash.isEmpty()) {
+				if (!certhash.toLowerCase().startsWith("sha256:"))
+					throw new ValidationError("certificate hash must start with 'sha256:'");
+				if (!hashPattern.matcher(certhash).matches())
+					throw new ValidationError("invalid certificate hash");
+			}
 
-            String binaryString = binary;
-            int binaryInt;
-            try {
-                binaryInt = (int) ParameterType.Int.parse(Parameter.BINARY.name, binaryString);
-            } catch (ValidationError e) {
-                try {
-                    boolean b = (boolean) ParameterType.Bool.parse(Parameter.BINARY.name, binaryString);
-                    binaryInt = b ? 65535 : 0;
-                } catch (ValidationError ee) {
-                    throw new ValidationError("binary= must be either a number or true/yes/on/false/no/off");
-                }
-            }
-            if (binaryInt < 0)
-                throw new ValidationError("binary= cannot be negative");
-            nbinary = binaryInt;
-
-
-            // 2. At least one of **sock** and **host** must be empty.
-            if (!sock.isEmpty() && !host.isEmpty())
-                throw new ValidationError("sock=" + sock + " cannot be combined with host=" + host);
+			// 6. If **tls** is 'off', **cert** and **certhash** must be 'off' as well.
+			if (!tls) {
+				if (!cert.isEmpty() || !certhash.isEmpty())
+					throw new ValidationError("cert= and certhash= are only allowed in combination with monetdbs://");
+			}
 
-            // 3. The string parameter **binary** must either parse as a boolean or as a
-            //    non-negative integer.
-            //
-            // (checked above)
-
-            // 4. If **sock** is not empty, **tls** must be 'off'.
-            if (!sock.isEmpty() && tls) throw new ValidationError("monetdbs:// cannot be combined with sock=");
-
-            // 5. If **certhash** is not empty, it must be of the form `{sha256}hexdigits`
-            //    where hexdigits is a non-empty sequence of 0-9, a-f, A-F and colons.
-            // TODO
-            if (!certhash.isEmpty()) {
-                if (!certhash.toLowerCase().startsWith("sha256:"))
-                    throw new ValidationError("certificate hash must start with 'sha256:'");
-                if (!hashPattern.matcher(certhash).matches())
-                    throw new ValidationError("invalid certificate hash");
-            }
-
-            // 6. If **tls** is 'off', **cert** and **certhash** must be 'off' as well.
-            if (!tls) {
-                if (!cert.isEmpty() || !certhash.isEmpty())
-                    throw new ValidationError("cert= and certhash= are only allowed in combination with monetdbs://");
-            }
-
-            // 7. Parameters **database**, **tableschema** and **table** must consist only of
-            //    upper- and lowercase letters, digits, periods, dashes and underscores. They must not
-            //    start with a dash.
-            //    If **table** is not empty, **tableschema** must also not be empty.
-            //    If **tableschema** is not empty, **database** must also not be empty.
-            if (database.isEmpty() && !tableschema.isEmpty())
-                throw new ValidationError("table schema cannot be set without database");
-            if (tableschema.isEmpty() && !table.isEmpty())
-                throw new ValidationError("table cannot be set without schema");
-            if (!database.isEmpty() && !namePattern.matcher(database).matches())
-                throw new ValidationError("invalid database name");
-            if (!tableschema.isEmpty() && !namePattern.matcher(tableschema).matches())
-                throw new ValidationError("invalid table schema name");
-            if (!table.isEmpty() && !namePattern.matcher(table).matches())
-                throw new ValidationError("invalid table name");
+			// 7. Parameters **database**, **tableschema** and **table** must consist only of
+			//    upper- and lowercase letters, digits, periods, dashes and underscores. They must not
+			//    start with a dash.
+			//    If **table** is not empty, **tableschema** must also not be empty.
+			//    If **tableschema** is not empty, **database** must also not be empty.
+			if (database.isEmpty() && !tableschema.isEmpty())
+				throw new ValidationError("table schema cannot be set without database");
+			if (tableschema.isEmpty() && !table.isEmpty())
+				throw new ValidationError("table cannot be set without schema");
+			if (!database.isEmpty() && !namePattern.matcher(database).matches())
+				throw new ValidationError("invalid database name");
+			if (!tableschema.isEmpty() && !namePattern.matcher(tableschema).matches())
+				throw new ValidationError("invalid table schema name");
+			if (!table.isEmpty() && !namePattern.matcher(table).matches())
+				throw new ValidationError("invalid table name");
 
 
-            // 8. Parameter **port** must be -1 or in the range 1-65535.
-            if (port < -1 || port == 0 || port > 65535) throw new ValidationError("invalid port number " + port);
+			// 8. Parameter **port** must be -1 or in the range 1-65535.
+			if (port < -1 || port == 0 || port > 65535)
+				throw new ValidationError("invalid port number " + port);
 
-            // 9. If **clientcert** is set, **clientkey** must also be set.
-            if (!clientcert.isEmpty() && clientkey.isEmpty())
-                throw new ValidationError("clientcert= is only valid in combination with clientkey=");
+			// 9. If **clientcert** is set, **clientkey** must also be set.
+			if (!clientcert.isEmpty() && clientkey.isEmpty())
+				throw new ValidationError("clientcert= is only valid in combination with clientkey=");
 
-            // JDBC specific
-            if (soTimeout < 0)
-                throw new ValidationError("so_timeout= must not be negative");
-        }
+			// JDBC specific
+			if (soTimeout < 0)
+				throw new ValidationError("so_timeout= must not be negative");
+		}
 
-        public boolean getTls() {
-            return tls;
-        }
+		public boolean getTls() {
+			return tls;
+		}
 
-        // Getter is private because you probably want connectTcp() instead
-        private String getHost() {
-            return host;
-        }
+		// Getter is private because you probably want connectTcp() instead
+		private String getHost() {
+			return host;
+		}
 
-        // Getter is private because you probably want connectPort() instead
-        private int getPort() {
-            return port;
-        }
+		// Getter is private because you probably want connectPort() instead
+		private int getPort() {
+			return port;
+		}
 
-        public String getDatabase() {
-            return database;
-        }
+		public String getDatabase() {
+			return database;
+		}
 
-        public String getTableschema() {
-            return tableschema;
-        }
+		public String getTableschema() {
+			return tableschema;
+		}
 
-        public String getTable() {
-            return table;
-        }
+		public String getTable() {
+			return table;
+		}
 
-        // Getter is private because you probably want connectUnix() instead
-        private String getSock() {
-            return sock;
-        }
+		// Getter is private because you probably want connectUnix() instead
+		private String getSock() {
+			return sock;
+		}
 
-        public String getSockdir() {
-            return sockdir;
-        }
+		public String getSockdir() {
+			return sockdir;
+		}
 
-        public String getCert() {
-            return cert;
-        }
+		public String getCert() {
+			return cert;
+		}
+
+		public String getCerthash() {
+			return certhash;
+		}
 
-        public String getCerthash() {
-            return certhash;
-        }
+		public String getClientkey() {
+			return clientkey;
+		}
 
-        public String getClientkey() {
-            return clientkey;
-        }
+		public String getClientcert() {
+			return clientcert;
+		}
 
-        public String getClientcert() {
-            return clientcert;
-        }
+		public String getUser() {
+			return user;
+		}
 
-        public String getUser() {
-            return user;
-        }
+		public String getPassword() {
+			return password;
+		}
 
-        public String getPassword() {
-            return password;
-        }
+		public String getLanguage() {
+			return language;
+		}
 
-        public String getLanguage() {
-            return language;
-        }
+		public boolean getAutocommit() {
+			return autocommit;
+		}
 
-        public boolean getAutocommit() {
-            return autocommit;
-        }
+		public String getSchema() {
+			return schema;
+		}
 
-        public String getSchema() {
-            return schema;
-        }
+		public int getTimezone() {
+			return timezone;
+		}
 
-        public int getTimezone() {
-            return timezone;
-        }
+		// Getter is private because you probably want connectBinary() instead
+		public int getBinary() {
+			return nbinary;
+		}
 
-        // Getter is private because you probably want connectBinary() instead
-        public int getBinary() {
-            return nbinary;
-        }
+		public int getReplySize() {
+			return replySize;
+		}
 
-        public int getReplySize() {
-            return replySize;
-        }
+		public String getHash() {
+			return hash;
+		}
 
-        public String getHash() {
-            return hash;
-        }
+		public boolean getDebug() {
+			return debug;
+		}
 
-        public boolean getDebug() {
-            return debug;
-        }
+		public String getLogfile() {
+			return logfile;
+		}
 
-        public String getLogfile() {
-            return logfile;
-        }
+		public int getSoTimeout() {
+			return soTimeout;
+		}
 
-        public int getSoTimeout() {
-            return soTimeout;
-        }
+		public boolean isTreatClobAsVarchar() {
+			return treatClobAsVarchar;
+		}
 
-        public boolean isTreatClobAsVarchar() {
-            return treatClobAsVarchar;
-        }
+		public boolean isTreatBlobAsBinary() {
+			return treatBlobAsBinary;
+		}
 
-        public boolean isTreatBlobAsBinary() {
-            return treatBlobAsBinary;
-        }
+		public boolean connectScan() {
+			if (database.isEmpty())
+				return false;
+			if (!sock.isEmpty() || !host.isEmpty() || port != -1)
+				return false;
+			return !tls;
+		}
 
-        public boolean connectScan() {
-            if (database.isEmpty()) return false;
-            if (!sock.isEmpty() || !host.isEmpty() || port != -1) return false;
-            return !tls;
-        }
-
-        public int connectPort() {
-            return port == -1 ? 50000 : port;
-        }
+		public int connectPort() {
+			return port == -1 ? 50000 : port;
+		}
 
-        public String connectUnix() {
-            if (!sock.isEmpty()) return sock;
-            if (tls) return "";
-            if (host.isEmpty()) return sockdir + "/.s.monetdb." + connectPort();
-            return "";
-        }
+		public String connectUnix() {
+			if (!sock.isEmpty())
+				return sock;
+			if (tls)
+				return "";
+			if (host.isEmpty())
+				return sockdir + "/.s.monetdb." + connectPort();
+			return "";
+		}
 
-        public String connectTcp() {
-            if (!sock.isEmpty()) return "";
-            if (host.isEmpty()) return "localhost";
-            return host;
-        }
-
-        public Verify connectVerify() {
-            if (!tls) return Verify.None;
-            if (!certhash.isEmpty()) return Verify.Hash;
-            if (!cert.isEmpty()) return Verify.Cert;
-            return Verify.System;
-        }
+		public String connectTcp() {
+			if (!sock.isEmpty())
+				return "";
+			if (host.isEmpty())
+				return "localhost";
+			return host;
+		}
 
-        public String connectCertHashDigits() {
-            if (!tls) return null;
-            StringBuilder builder = new StringBuilder(certhash.length());
-            for (int i = "sha256:".length(); i < certhash.length(); i++) {
-                char c = certhash.charAt(i);
-                if (Character.digit(c, 16) >= 0) builder.append(Character.toLowerCase(c));
-            }
-            return builder.toString();
-        }
-
-        public int connectBinary() {
-            return nbinary;
-        }
+		public Verify connectVerify() {
+			if (!tls)
+				return Verify.None;
+			if (!certhash.isEmpty())
+				return Verify.Hash;
+			if (!cert.isEmpty())
+				return Verify.Cert;
+			return Verify.System;
+		}
 
-        public String connectClientKey() {
-            return clientkey;
-        }
+		public String connectCertHashDigits() {
+			if (!tls)
+				return null;
+			StringBuilder builder = new StringBuilder(certhash.length());
+			for (int i = "sha256:".length(); i < certhash.length(); i++) {
+				char c = certhash.charAt(i);
+				if (Character.digit(c, 16) >= 0)
+					builder.append(Character.toLowerCase(c));
+			}
+			return builder.toString();
+		}
 
-        public String connectClientCert() {
-            return clientcert.isEmpty() ? clientkey : clientcert;
-        }
-    }
+		public int connectBinary() {
+			return nbinary;
+		}
 
-    public enum Verify {
-        None,
-        Cert,
-        Hash,
-        System;
-    }
+		public String connectClientKey() {
+			return clientkey;
+		}
+
+		public String connectClientCert() {
+			return clientcert.isEmpty() ? clientkey : clientcert;
+		}
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/ValidationError.java
+++ b/src/main/java/org/monetdb/mcl/net/ValidationError.java
@@ -1,11 +1,11 @@
 package org.monetdb.mcl.net;
 
 public class ValidationError extends Exception {
-    public ValidationError(String parameter, String message) {
-        super(parameter + ": " + message);
-    }
+	public ValidationError(String parameter, String message) {
+		super(parameter + ": " + message);
+	}
 
-    public ValidationError(String message) {
-        super(message);
-    }
+	public ValidationError(String message) {
+		super(message);
+	}
 }
--- a/tests/TLSTester.java
+++ b/tests/TLSTester.java
@@ -11,307 +11,304 @@ import java.sql.SQLException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Properties;
-import java.util.stream.Collectors;
 
 public class TLSTester {
-    int verbose = 0;
-    String serverHost = null;
-    String altHost = null;
-    int serverPort = -1;
-    boolean enableTrusted = false;
-    File tempDir = null;
-    final HashMap<String, File> fileCache = new HashMap<>();
-    private HashSet<String> preparedButNotRun = new HashSet<>();
+	final HashMap<String, File> fileCache = new HashMap<>();
+	int verbose = 0;
+	String serverHost = null;
+	String altHost = null;
+	int serverPort = -1;
+	boolean enableTrusted = false;
+	File tempDir = null;
+	private final HashSet<String> preparedButNotRun = new HashSet<>();
 
-    public TLSTester(String[] args) {
-        for (int i = 0; i < args.length; i++) {
-            String arg = args[i];
-            if (arg.equals("-v")) {
-                verbose = 1;
-            } else if (arg.equals("-a")) {
-                altHost = args[++i];
-            } else if (arg.equals("-t")) {
-                enableTrusted = true;
-            } else if (!arg.startsWith("-") && serverHost == null) {
-                int idx = arg.indexOf(':');
-                if (idx > 0) {
-                    serverHost = arg.substring(0, idx);
-                    try {
-                        serverPort = Integer.parseInt(arg.substring(idx + 1));
-                        if (serverPort > 0 && serverPort < 65536)
-                            continue;
-                    } catch (NumberFormatException ignored) {
-                    }
-                }
-                // if we get here it wasn't very valid
-                throw new IllegalArgumentException("Invalid argument: " + arg);
-            } else {
-                throw new IllegalArgumentException("Unexpected argument: " + arg);
-            }
-        }
-    }
+	public TLSTester(String[] args) {
+		for (int i = 0; i < args.length; i++) {
+			String arg = args[i];
+			if (arg.equals("-v")) {
+				verbose = 1;
+			} else if (arg.equals("-a")) {
+				altHost = args[++i];
+			} else if (arg.equals("-t")) {
+				enableTrusted = true;
+			} else if (!arg.startsWith("-") && serverHost == null) {
+				int idx = arg.indexOf(':');
+				if (idx > 0) {
+					serverHost = arg.substring(0, idx);
+					try {
+						serverPort = Integer.parseInt(arg.substring(idx + 1));
+						if (serverPort > 0 && serverPort < 65536)
+							continue;
+					} catch (NumberFormatException ignored) {
+					}
+				}
+				// if we get here it wasn't very valid
+				throw new IllegalArgumentException("Invalid argument: " + arg);
+			} else {
+				throw new IllegalArgumentException("Unexpected argument: " + arg);
+			}
+		}
+	}
 
-    public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {
-        Class.forName("org.monetdb.jdbc.MonetDriver");
-        TLSTester main = new TLSTester(args);
-        main.run();
-    }
+	public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {
+		Class.forName("org.monetdb.jdbc.MonetDriver");
+		TLSTester main = new TLSTester(args);
+		main.run();
+	}
 
-    private HashMap<String,Integer> loadPortMap(String testName) throws IOException {
-        HashMap<String,Integer> portMap = new HashMap<>();
-        InputStream in = fetchData("/?test=" + testName);
-        BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
-        for (String line = br.readLine(); line != null; line = br.readLine()) {
-            int idx = line.indexOf(':');
-            String service = line.substring(0, idx);
-            int port;
-            try {
-                port = Integer.parseInt(line.substring(idx + 1));
-            } catch (NumberFormatException e) {
-                throw new RuntimeException("Invalid port map line: " + line);
-            }
-            portMap.put(service, port);
-        }
-        return portMap;
-    }
+	private HashMap<String, Integer> loadPortMap(String testName) throws IOException {
+		HashMap<String, Integer> portMap = new HashMap<>();
+		InputStream in = fetchData("/?test=" + testName);
+		BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+		for (String line = br.readLine(); line != null; line = br.readLine()) {
+			int idx = line.indexOf(':');
+			String service = line.substring(0, idx);
+			int port;
+			try {
+				port = Integer.parseInt(line.substring(idx + 1));
+			} catch (NumberFormatException e) {
+				throw new RuntimeException("Invalid port map line: " + line);
+			}
+			portMap.put(service, port);
+		}
+		return portMap;
+	}
 
-    private File resource(String resource) throws IOException {
-        if (!fileCache.containsKey(resource))
-            fetchResource(resource);
-        return fileCache.get(resource);
-    }
+	private File resource(String resource) throws IOException {
+		if (!fileCache.containsKey(resource))
+			fetchResource(resource);
+		return fileCache.get(resource);
+	}
 
-    private void fetchResource(String resource) throws IOException {
-        if (!resource.startsWith("/")) {
-            throw new IllegalArgumentException("Resource must start with slash: " + resource);
-        }
-        if (tempDir == null) {
-            tempDir = Files.createTempDirectory("tlstest").toFile();
-            tempDir.deleteOnExit();
-        }
-        File outPath = new File(tempDir, resource.substring(1));
-        try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) {
-            byte[] buffer = new byte[12];
-            while (true) {
-                int n = in.read(buffer);
-                if (n <= 0)
-                    break;
-                out.write(buffer, 0, n);
-            }
-        }
-        fileCache.put(resource, outPath);
-    }
+	private void fetchResource(String resource) throws IOException {
+		if (!resource.startsWith("/")) {
+			throw new IllegalArgumentException("Resource must start with slash: " + resource);
+		}
+		if (tempDir == null) {
+			tempDir = Files.createTempDirectory("tlstest").toFile();
+			tempDir.deleteOnExit();
+		}
+		File outPath = new File(tempDir, resource.substring(1));
+		try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) {
+			byte[] buffer = new byte[12];
+			while (true) {
+				int n = in.read(buffer);
+				if (n <= 0)
+					break;
+				out.write(buffer, 0, n);
+			}
+		}
+		fileCache.put(resource, outPath);
+	}
 
-    private byte[] fetchBytes(String resource) throws IOException {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        try (InputStream in = fetchData(resource)) {
-            byte[] buffer = new byte[22];
-            while (true) {
-                int nread = in.read(buffer);
-                if (nread <= 0)
-                    break;
-                out.write(buffer, 0, nread);
-            }
-            return out.toByteArray();
-        }
-    }
+	private byte[] fetchBytes(String resource) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		try (InputStream in = fetchData(resource)) {
+			byte[] buffer = new byte[22];
+			while (true) {
+				int nread = in.read(buffer);
+				if (nread <= 0)
+					break;
+				out.write(buffer, 0, nread);
+			}
+			return out.toByteArray();
+		}
+	}
 
-    private InputStream fetchData(String resource) throws IOException {
-        URL url = new URL("http://" + serverHost + ":" + serverPort + resource);
-        URLConnection conn = url.openConnection();
-        conn.connect();
-        return conn.getInputStream();
-    }
+	private InputStream fetchData(String resource) throws IOException {
+		URL url = new URL("http://" + serverHost + ":" + serverPort + resource);
+		URLConnection conn = url.openConnection();
+		conn.connect();
+		return conn.getInputStream();
+	}
 
-    private void run() throws IOException, SQLException {
-        test_connect_plain();
-        test_connect_tls();
-        test_refuse_no_cert();
-        test_refuse_wrong_cert();
-        test_refuse_wrong_host();
-        test_refuse_tlsv12();
-        test_refuse_expired();
+	private void run() throws IOException, SQLException {
+		test_connect_plain();
+		test_connect_tls();
+		test_refuse_no_cert();
+		test_refuse_wrong_cert();
+		test_refuse_wrong_host();
+		test_refuse_tlsv12();
+		test_refuse_expired();
 //        test_connect_client_auth1();
 //        test_connect_client_auth2();
-        test_fail_tls_to_plain();
-        test_fail_plain_to_tls();
-        test_connect_server_name();
-        test_connect_alpn_mapi9();
-        test_connect_trusted();
-        test_refuse_trusted_wrong_host();
+		test_fail_tls_to_plain();
+		test_fail_plain_to_tls();
+		test_connect_server_name();
+		test_connect_alpn_mapi9();
+		test_connect_trusted();
+		test_refuse_trusted_wrong_host();
 
-        // did we forget to call expectSucceed and expectFailure somewhere?
-        if (!preparedButNotRun.isEmpty()) {
-            String names = String.join(", ", preparedButNotRun);
-            throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names);
-        }
-    }
+		// did we forget to call expectSucceed and expectFailure somewhere?
+		if (!preparedButNotRun.isEmpty()) {
+			String names = String.join(", ", preparedButNotRun);
+			throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names);
+		}
+	}
 
-    private void test_connect_plain() throws IOException, SQLException {
-        attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess();
-    }
+	private void test_connect_plain() throws IOException, SQLException {
+		attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess();
+	}
 
-    private void test_connect_tls() throws IOException, SQLException {
-        Attempt attempt = attempt("connect_tls", "server1");
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
-    }
+	private void test_connect_tls() throws IOException, SQLException {
+		Attempt attempt = attempt("connect_tls", "server1");
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
+	}
 
-    private void test_refuse_no_cert() throws IOException, SQLException {
-        attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed");
-    }
+	private void test_refuse_no_cert() throws IOException, SQLException {
+		attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed");
+	}
 
-    private void test_refuse_wrong_cert() throws IOException, SQLException {
-        Attempt attempt = attempt("refuse_wrong_cert", "server1");
-        attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed");
-    }
+	private void test_refuse_wrong_cert() throws IOException, SQLException {
+		Attempt attempt = attempt("refuse_wrong_cert", "server1");
+		attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed");
+	}
 
-    private void test_refuse_wrong_host() throws IOException, SQLException {
-        Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost);
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name");
-    }
+	private void test_refuse_wrong_host() throws IOException, SQLException {
+		Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost);
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name");
+	}
 
-    private void test_refuse_tlsv12() throws IOException, SQLException {
-        Attempt attempt = attempt("refuse_tlsv12", "tls12");
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version");
-    }
+	private void test_refuse_tlsv12() throws IOException, SQLException {
+		Attempt attempt = attempt("refuse_tlsv12", "tls12");
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version");
+	}
 
-    private void test_refuse_expired() throws IOException, SQLException {
-        Attempt attempt = attempt("refuse_expired", "expiredcert");
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed");
-    }
-
-    private void test_connect_client_auth1() throws IOException, SQLException {
-        attempt("connect_client_auth1", "clientauth")
-                .withFile(Parameter.CERT, "/ca1.crt")
-                .withFile(Parameter.CLIENTKEY, "/client2.keycrt")
-                .expectSuccess();
-    }
+	private void test_refuse_expired() throws IOException, SQLException {
+		Attempt attempt = attempt("refuse_expired", "expiredcert");
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed");
+	}
 
-    private void test_connect_client_auth2() throws IOException, SQLException {
-        attempt("connect_client_auth2", "clientauth")
-                .withFile(Parameter.CERT, "/ca1.crt")
-                .withFile(Parameter.CLIENTKEY, "/client2.key")
-                .withFile(Parameter.CLIENTCERT, "/client2.crt")
-                .expectSuccess();
-    }
+	private void test_connect_client_auth1() throws IOException, SQLException {
+		attempt("connect_client_auth1", "clientauth")
+				.withFile(Parameter.CERT, "/ca1.crt")
+				.withFile(Parameter.CLIENTKEY, "/client2.keycrt")
+				.expectSuccess();
+	}
 
-    private void test_fail_tls_to_plain() throws IOException, SQLException {
-        Attempt attempt = attempt("fail_tls_to_plain", "plain");
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("");
+	private void test_connect_client_auth2() throws IOException, SQLException {
+		attempt("connect_client_auth2", "clientauth")
+				.withFile(Parameter.CERT, "/ca1.crt")
+				.withFile(Parameter.CLIENTKEY, "/client2.key")
+				.withFile(Parameter.CLIENTCERT, "/client2.crt")
+				.expectSuccess();
+	}
 
-    }
+	private void test_fail_tls_to_plain() throws IOException, SQLException {
+		Attempt attempt = attempt("fail_tls_to_plain", "plain");
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("");
 
-    private void test_fail_plain_to_tls() throws IOException, SQLException {
-        attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect");
-    }
+	}
 
-    private void test_connect_server_name() throws IOException, SQLException {
-        Attempt attempt = attempt("connect_server_name", "sni");
-        attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
-    }
+	private void test_fail_plain_to_tls() throws IOException, SQLException {
+		attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect");
+	}
 
-    private void test_connect_alpn_mapi9() throws IOException, SQLException {
-        attempt("connect_alpn_mapi9", "alpn_mapi9")
-                .withFile(Parameter.CERT, "/ca1.crt")
-                .expectSuccess();
-    }
+	private void test_connect_server_name() throws IOException, SQLException {
+		Attempt attempt = attempt("connect_server_name", "sni");
+		attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
+	}
 
-    private void test_connect_trusted() throws IOException, SQLException {
-        attempt("connect_trusted", null)
-                .with(Parameter.HOST, "monetdb.ergates.nl")
-                .with(Parameter.PORT, 50000)
-                .expectSuccess();
-    }
+	private void test_connect_alpn_mapi9() throws IOException, SQLException {
+		attempt("connect_alpn_mapi9", "alpn_mapi9").withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
+	}
+
+	private void test_connect_trusted() throws IOException, SQLException {
+		attempt("connect_trusted", null)
+				.with(Parameter.HOST, "monetdb.ergates.nl")
+				.with(Parameter.PORT, 50000)
+				.expectSuccess();
+	}
 
-    private void test_refuse_trusted_wrong_host() throws IOException, SQLException {
-        attempt("test_refuse_trusted_wrong_host", null)
-                .with(Parameter.HOST, "monetdbxyz.ergates.nl")
-                .with(Parameter.PORT, 50000)
-                .expectFailure("No subject alternative DNS name");
-    }
+	private void test_refuse_trusted_wrong_host() throws IOException, SQLException {
+		attempt("test_refuse_trusted_wrong_host", null)
+				.with(Parameter.HOST, "monetdbxyz.ergates.nl")
+				.with(Parameter.PORT, 50000)
+				.expectFailure("No subject alternative DNS name");
+	}
 
-    private Attempt attempt(String testName, String portName) throws IOException {
-        preparedButNotRun.add(testName);
-        return new Attempt(testName, portName);
-    }
+	private Attempt attempt(String testName, String portName) throws IOException {
+		preparedButNotRun.add(testName);
+		return new Attempt(testName, portName);
+	}
 
-    private class Attempt {
-        private final String testName;
-        private final Properties props = new Properties();
-        boolean disabled = false;
+	private class Attempt {
+		private final String testName;
+		private final Properties props = new Properties();
+		boolean disabled = false;
 
-        public Attempt(String testName, String portName) throws IOException {
-            HashMap<String, Integer> portMap = loadPortMap(testName);
+		public Attempt(String testName, String portName) throws IOException {
+			HashMap<String, Integer> portMap = loadPortMap(testName);
 
-            this.testName = testName;
-            with(Parameter.TLS, true);
-            with(Parameter.HOST, serverHost);
-            with(Parameter.SO_TIMEOUT, 3000);
-            if (portName != null) {
-                Integer port = portMap.get(portName);
-                if (port != null) {
-                    with(Parameter.PORT, port);
-                } else {
-                    throw new RuntimeException("Unknown port name: " + portName);
-                }
-            }
-        }
+			this.testName = testName;
+			with(Parameter.TLS, true);
+			with(Parameter.HOST, serverHost);
+			with(Parameter.SO_TIMEOUT, 3000);
+			if (portName != null) {
+				Integer port = portMap.get(portName);
+				if (port != null) {
+					with(Parameter.PORT, port);
+				} else {
+					throw new RuntimeException("Unknown port name: " + portName);
+				}
+			}
+		}
 
-        private Attempt with(Parameter parm, String value) {
-            props.setProperty(parm.name, value);
-            return this;
-        }
+		private Attempt with(Parameter parm, String value) {
+			props.setProperty(parm.name, value);
+			return this;
+		}
 
-        private Attempt with(Parameter parm, int value) {
-            props.setProperty(parm.name, Integer.toString(value));
-            return this;
-        }
+		private Attempt with(Parameter parm, int value) {
+			props.setProperty(parm.name, Integer.toString(value));
+			return this;
+		}
 
-        private Attempt with(Parameter parm, boolean value) {
-            props.setProperty(parm.name, value ? "true" : "false");
-            return this;
-        }
+		private Attempt with(Parameter parm, boolean value) {
+			props.setProperty(parm.name, value ? "true" : "false");
+			return this;
+		}
 
-        private Attempt withFile(Parameter parm, String certResource) throws IOException {
-            File certFile = resource(certResource);
-            String path = certFile.getPath();
-            with(parm, path);
-            return this;
-        }
+		private Attempt withFile(Parameter parm, String certResource) throws IOException {
+			File certFile = resource(certResource);
+			String path = certFile.getPath();
+			with(parm, path);
+			return this;
+		}
 
-        public void expectSuccess() throws SQLException {
-            preparedButNotRun.remove(testName);
-            if (disabled)
-                return;
-            try {
-                Connection conn = DriverManager.getConnection("jdbc:monetdb:", props);
-                conn.close();
-            } catch (SQLException e) {
-                if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) {
-                    // it looks like a failure but this is actually our success scenario
-                    // because this is what the TLS Tester does when the connection succeeds.
-                    return;
-                }
-                // other exceptions ARE errors and should be reported.
-                throw e;
-            }
-        }
+		public void expectSuccess() throws SQLException {
+			preparedButNotRun.remove(testName);
+			if (disabled)
+				return;
+			try {
+				Connection conn = DriverManager.getConnection("jdbc:monetdb:", props);
+				conn.close();
+			} catch (SQLException e) {
+				if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) {
+					// it looks like a failure but this is actually our success scenario
+					// because this is what the TLS Tester does when the connection succeeds.
+					return;
+				}
+				// other exceptions ARE errors and should be reported.
+				throw e;
+			}
+		}
 
-        public void expectFailure(String... expectedMessages) throws SQLException {
-            if (disabled)
-                return;
-            try {
-                expectSuccess();
-                throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't");
-            } catch (SQLException e) {
-                for (String expected : expectedMessages)
-                    if (e.getMessage().contains(expected))
-                        return;
-                String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n        <" + String.join(">\n        <", expectedMessages) + ">";
-                throw new RuntimeException(message);
+		public void expectFailure(String... expectedMessages) throws SQLException {
+			if (disabled)
+				return;
+			try {
+				expectSuccess();
+				throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't");
+			} catch (SQLException e) {
+				for (String expected : expectedMessages)
+					if (e.getMessage().contains(expected))
+						return;
+				String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n        <" + String.join(">\n        <", expectedMessages) + ">";
+				throw new RuntimeException(message);
 
-            }
-        }
+			}
+		}
 
-    }
+	}
 }
--- a/tests/UrlTester.java
+++ b/tests/UrlTester.java
@@ -5,410 +5,410 @@ import java.net.URISyntaxException;
 import java.util.ArrayList;
 
 public class UrlTester {
-    final String filename;
-    final int verbose;
-    final BufferedReader reader;
-    int lineno = 0;
-    int testCount = 0;
-    Target target = null;
-    Target.Validated validated = null;
+	final String filename;
+	final int verbose;
+	final BufferedReader reader;
+	int lineno = 0;
+	int testCount = 0;
+	Target target = null;
+	Target.Validated validated = null;
 
-    public UrlTester(String filename, BufferedReader reader, int verbose) {
-        this.filename = filename;
-        this.verbose = verbose;
-        this.reader = reader;
-    }
+	public UrlTester(String filename, BufferedReader reader, int verbose) {
+		this.filename = filename;
+		this.verbose = verbose;
+		this.reader = reader;
+	}
 
-    public UrlTester(String filename, int verbose) throws IOException {
-        this.filename = filename;
-        this.verbose = verbose;
-        this.reader = new BufferedReader(new FileReader(filename));
-    }
+	public UrlTester(String filename, int verbose) throws IOException {
+		this.filename = filename;
+		this.verbose = verbose;
+		this.reader = new BufferedReader(new FileReader(filename));
+	}
 
-    public static void main(String[] args) throws IOException {
-        ArrayList<String> filenames = new ArrayList<>();
-        int verbose = 0;
-        for (String arg : args) {
-            switch (arg) {
-                case "-vvv":
-                    verbose++;
-                case "-vv":
-                    verbose++;
-                case "-v":
-                    verbose++;
-                    break;
-                case "-h":
-                case "--help":
-                    exitUsage(null);
-                    break;
-                default:
-                    if (!arg.startsWith("-")) {
-                        filenames.add(arg);
-                    } else {
-                        exitUsage("Unexpected argument: " + arg);
-                    }
-                    break;
-            }
-        }
+	public static void main(String[] args) throws IOException {
+		ArrayList<String> filenames = new ArrayList<>();
+		int verbose = 0;
+		for (String arg : args) {
+			switch (arg) {
+				case "-vvv":
+					verbose++;
+				case "-vv":
+					verbose++;
+				case "-v":
+					verbose++;
+					break;
+				case "-h":
+				case "--help":
+					exitUsage(null);
+					break;
+				default:
+					if (!arg.startsWith("-")) {
+						filenames.add(arg);
+					} else {
+						exitUsage("Unexpected argument: " + arg);
+					}
+					break;
+			}
+		}
 
-        runUnitTests();
+		runUnitTests();
 
-        try {
-            if (filenames.isEmpty()) {
-                runAllTests();
-            } else {
-                for (String filename : filenames) {
-                    new UrlTester(filename, verbose).run();
-                }
-            }
-        } catch (Failure e) {
-            System.err.println("Test failed: " + e.getMessage());
-            System.exit(1);
-        }
-    }
+		try {
+			if (filenames.isEmpty()) {
+				runAllTests();
+			} else {
+				for (String filename : filenames) {
+					new UrlTester(filename, verbose).run();
+				}
+			}
+		} catch (Failure e) {
+			System.err.println("Test failed: " + e.getMessage());
+			System.exit(1);
+		}
+	}
 
-    private static void exitUsage(String message) {
-        if (message != null) {
-            System.err.println(message);
-        }
-        System.err.println("Usage: UrlTester OPTIONS [FILENAME..]");
-        System.err.println("Options:");
-        System.err.println("   -v        Be more verbose");
-        System.err.println("   -h --help Show this help");
-        int status = message == null ? 0 : 1;
-        System.exit(status);
-    }
+	private static void exitUsage(String message) {
+		if (message != null) {
+			System.err.println(message);
+		}
+		System.err.println("Usage: UrlTester OPTIONS [FILENAME..]");
+		System.err.println("Options:");
+		System.err.println("   -v        Be more verbose");
+		System.err.println("   -h --help Show this help");
+		int status = message == null ? 0 : 1;
+		System.exit(status);
+	}
 
-    public static UrlTester forResource(String resourceName, int verbose) throws FileNotFoundException {
-        InputStream stream = UrlTester.class.getResourceAsStream(resourceName);
-        if (stream == null) {
-            throw new FileNotFoundException("Resource " + resourceName);
-        }
-        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
-        return new UrlTester(resourceName, reader, verbose);
-    }
+	public static UrlTester forResource(String resourceName, int verbose) throws FileNotFoundException {
+		InputStream stream = UrlTester.class.getResourceAsStream(resourceName);
+		if (stream == null) {
+			throw new FileNotFoundException("Resource " + resourceName);
+		}
+		BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+		return new UrlTester(resourceName, reader, verbose);
+	}
 
-    public static void runAllTests() throws IOException, Failure {
-        runUnitTests();
-        UrlTester.forResource("/tests.md", 0).run();
-        UrlTester.forResource("/javaspecific.md", 0).run();
-    }
+	public static void runAllTests() throws IOException, Failure {
+		runUnitTests();
+		UrlTester.forResource("/tests.md", 0).run();
+		UrlTester.forResource("/javaspecific.md", 0).run();
+	}
 
-    public static void runUnitTests() {
-        testDefaults();
-        testParameters();
-    }
+	public static void runUnitTests() {
+		testDefaults();
+		testParameters();
+	}
 
-    private static void testDefaults() {
-        Target target = new Target();
+	private static void testDefaults() {
+		Target target = new Target();
 
-        for (Parameter parm : Parameter.values()) {
-            Object expected = parm.getDefault();
-            if (expected == null)
-                continue;
-            Object actual = target.getObject(parm);
-            if (!expected.equals(actual)) {
-                throw new RuntimeException("Default for " + parm.name + " expected to be <" + expected + "> but is <" + actual + ">");
-            }
-        }
-    }
+		for (Parameter parm : Parameter.values()) {
+			Object expected = parm.getDefault();
+			if (expected == null)
+				continue;
+			Object actual = target.getObject(parm);
+			if (!expected.equals(actual)) {
+				throw new RuntimeException("Default for " + parm.name + " expected to be <" + expected + "> but is <" + actual + ">");
+			}
+		}
+	}
 
-    private static void testParameters() {
-        for (Parameter parm : Parameter.values()) {
-            Parameter found = Parameter.forName(parm.name);
-            if (parm != found) {
-                String foundStr = found != null ? found.name : "null";
-                throw new RuntimeException("Looking up <" + parm.name + ">, found <" + foundStr);
-            }
-        }
-    }
+	private static void testParameters() {
+		for (Parameter parm : Parameter.values()) {
+			Parameter found = Parameter.forName(parm.name);
+			if (parm != found) {
+				String foundStr = found != null ? found.name : "null";
+				throw new RuntimeException("Looking up <" + parm.name + ">, found <" + foundStr);
+			}
+		}
+	}
 
-    public void run() throws Failure, IOException {
-        try {
-            processFile();
-        } catch (Failure e) {
-            if (e.getFilename() == null) {
-                e.setFilename(filename);
-                e.setLineno(lineno);
-                throw e;
-            }
-        }
-    }
+	public void run() throws Failure, IOException {
+		try {
+			processFile();
+		} catch (Failure e) {
+			if (e.getFilename() == null) {
+				e.setFilename(filename);
+				e.setLineno(lineno);
+				throw e;
+			}
+		}
+	}
 
-    private void processFile() throws IOException, Failure {
-        while (true) {
-            String line = reader.readLine();
-            if (line == null)
-                break;
-            lineno++;
-            processLine(line);
-        }
-        if (verbose >= 1) {
-            System.out.println();
-            System.out.println("Ran " + testCount + " tests in " + lineno + " lines");
-        }
-    }
+	private void processFile() throws IOException, Failure {
+		while (true) {
+			String line = reader.readLine();
+			if (line == null)
+				break;
+			lineno++;
+			processLine(line);
+		}
+		if (verbose >= 1) {
+			System.out.println();
+			System.out.println("Ran " + testCount + " tests in " + lineno + " lines");
+		}
+	}
 
-    private void processLine(String line) throws Failure {
-        line = line.replaceFirst("\\s+$", ""); // remove trailing
-        if (target == null && line.equals("```test")) {
-            if (verbose >= 2) {
-                if (testCount > 0) {
-                    System.out.println();
-                }
-                System.out.println("\u25B6 " + filename + ":" + lineno);
-            }
-            target = new Target();
-            testCount++;
-            return;
-        }
-        if (target != null) {
-            if (line.equals("```")) {
-                stopProcessing();
-                return;
-            }
-            handleCommand(line);
-        }
-    }
+	private void processLine(String line) throws Failure {
+		line = line.replaceFirst("\\s+$", ""); // remove trailing
+		if (target == null && line.equals("```test")) {
+			if (verbose >= 2) {
+				if (testCount > 0) {
+					System.out.println();
+				}
+				System.out.println("\u25B6 " + filename + ":" + lineno);
+			}
+			target = new Target();
+			testCount++;
+			return;
+		}
+		if (target != null) {
+			if (line.equals("```")) {
+				stopProcessing();
+				return;
+			}
+			handleCommand(line);
+		}
+	}
 
-    private void stopProcessing() {
-        target = null;
-        validated = null;
-    }
+	private void stopProcessing() {
+		target = null;
+		validated = null;
+	}
 
-    private void handleCommand(String line) throws Failure {
-        if (verbose >= 3) {
-            System.out.println(line);
-        }
-        if (line.isEmpty())
-            return;
+	private void handleCommand(String line) throws Failure {
+		if (verbose >= 3) {
+			System.out.println(line);
+		}
+		if (line.isEmpty())
+			return;
 
-        String[] parts = line.split("\\s+", 2);
-        String command = parts[0];
-        switch (command.toUpperCase()) {
-            case "ONLY":
-                handleOnly(true, parts[1]);
-                return;
-            case "NOT":
-                handleOnly(false, parts[1]);
-                return;
-            case "PARSE":
-                handleParse(parts[1], null);
-                return;
-            case "ACCEPT":
-                handleParse(parts[1], true);
-                return;
-            case "REJECT":
-                handleParse(parts[1], false);
-                return;
-            case "SET":
-                handleSet(parts[1]);
-                return;
-            case "EXPECT":
-                handleExpect(parts[1]);
-                return;
-            default:
-                throw new Failure("Unexpected command: " + command);
-        }
+		String[] parts = line.split("\\s+", 2);
+		String command = parts[0];
+		switch (command.toUpperCase()) {
+			case "ONLY":
+				handleOnly(true, parts[1]);
+				return;
+			case "NOT":
+				handleOnly(false, parts[1]);
+				return;
+			case "PARSE":
+				handleParse(parts[1], null);
+				return;
+			case "ACCEPT":
+				handleParse(parts[1], true);
+				return;
+			case "REJECT":
+				handleParse(parts[1], false);
+				return;
+			case "SET":
+				handleSet(parts[1]);
+				return;
+			case "EXPECT":
+				handleExpect(parts[1]);
+				return;
+			default:
+				throw new Failure("Unexpected command: " + command);
+		}
 
-    }
+	}
 
-    private void handleOnly(boolean mustBePresent, String rest) throws Failure {
-        boolean found = false;
-        for (String part : rest.split("\\s+")) {
-            if (part.equals("jdbc")) {
-                found = true;
-                break;
-            }
-        }
-        if (found != mustBePresent) {
-            // do not further process this block
-            stopProcessing();
-        }
-    }
+	private void handleOnly(boolean mustBePresent, String rest) throws Failure {
+		boolean found = false;
+		for (String part : rest.split("\\s+")) {
+			if (part.equals("jdbc")) {
+				found = true;
+				break;
+			}
+		}
+		if (found != mustBePresent) {
+			// do not further process this block
+			stopProcessing();
+		}
+	}
 
-    private int findEqualSign(String rest) throws Failure {
-        int index = rest.indexOf('=');
-        if (index < -1)
-            throw new Failure("Expected to find a '='");
-        return index;
-    }
+	private int findEqualSign(String rest) throws Failure {
+		int index = rest.indexOf('=');
+		if (index < -1)
+			throw new Failure("Expected to find a '='");
+		return index;
+	}
 
-    private String splitKey(String rest) throws Failure {
-        int index = findEqualSign(rest);
-        return rest.substring(0, index);
-    }
+	private String splitKey(String rest) throws Failure {
+		int index = findEqualSign(rest);
+		return rest.substring(0, index);
+	}
 
-    private String splitValue(String rest) throws Failure {
-        int index = findEqualSign(rest);
-        return rest.substring(index + 1);
-    }
+	private String splitValue(String rest) throws Failure {
+		int index = findEqualSign(rest);
+		return rest.substring(index + 1);
+	}
 
-    private void handleSet(String rest) throws Failure {
-        validated = null;
-        String key = splitKey(rest);
-        String value = splitValue(rest);
+	private void handleSet(String rest) throws Failure {
+		validated = null;
+		String key = splitKey(rest);
+		String value = splitValue(rest);
 
-        try {
-            target.setString(key, value);
-        } catch (ValidationError e) {
-            throw new Failure(e.getMessage());
-        }
-    }
+		try {
+			target.setString(key, value);
+		} catch (ValidationError e) {
+			throw new Failure(e.getMessage());
+		}
+	}
 
-    private void handleParse(String rest, Boolean shouldSucceed) throws Failure {
-        URISyntaxException parseError = null;
-        ValidationError validationError = null;
+	private void handleParse(String rest, Boolean shouldSucceed) throws Failure {
+		URISyntaxException parseError = null;
+		ValidationError validationError = null;
 
-        validated = null;
-        try {
-            target.barrier();
-            MonetUrlParser.parse(target, rest);
-        } catch (URISyntaxException e) {
-            parseError = e;
-        } catch (ValidationError e) {
-            validationError = e;
-        }
+		validated = null;
+		try {
+			target.barrier();
+			MonetUrlParser.parse(target, rest);
+		} catch (URISyntaxException e) {
+			parseError = e;
+		} catch (ValidationError e) {
+			validationError = e;
+		}
 
-        if (parseError == null && validationError == null) {
-            try {
-                tryValidate();
-            } catch (ValidationError e) {
-                validationError = e;
-            }
-        }
+		if (parseError == null && validationError == null) {
+			try {
+				tryValidate();
+			} catch (ValidationError e) {
+				validationError = e;
+			}
+		}
 
-        if (shouldSucceed == Boolean.FALSE) {
-            if (parseError != null || validationError != null)
-                return; // happy
-            else
-                throw new Failure("URL unexpectedly parsed and validated");
-        }
+		if (shouldSucceed == Boolean.FALSE) {
+			if (parseError != null || validationError != null)
+				return; // happy
+			else
+				throw new Failure("URL unexpectedly parsed and validated");
+		}
 
-        if (parseError != null)
-            throw new Failure("Parse error: " + parseError);
-        if (validationError != null && shouldSucceed == Boolean.TRUE)
-            throw new Failure("Validation error: " + validationError);
-    }
+		if (parseError != null)
+			throw new Failure("Parse error: " + parseError);
+		if (validationError != null && shouldSucceed == Boolean.TRUE)
+			throw new Failure("Validation error: " + validationError);
+	}
 
-    private void handleExpect(String rest) throws Failure {
-        String key = splitKey(rest);
-        String expectedString = splitValue(rest);
+	private void handleExpect(String rest) throws Failure {
+		String key = splitKey(rest);
+		String expectedString = splitValue(rest);
 
-        Object actual = null;
-        try {
-            actual = extract(key);
-        } catch (ValidationError e) {
-            throw new Failure(e.getMessage());
-        }
+		Object actual = null;
+		try {
+			actual = extract(key);
+		} catch (ValidationError e) {
+			throw new Failure(e.getMessage());
+		}
 
-        Object expected;
-        try {
-            if (actual instanceof Boolean)
-                expected = ParameterType.Bool.parse(key, expectedString);
-            else if (actual instanceof Integer)
-                expected = ParameterType.Int.parse(key, expectedString);
-            else
-                expected = expectedString;
-        } catch (ValidationError e) {
-            String typ = actual.getClass().getName();
-            throw new Failure("Cannot convert expected value <" + expectedString + "> to " + typ + ": " + e.getMessage());
-        }
+		Object expected;
+		try {
+			if (actual instanceof Boolean)
+				expected = ParameterType.Bool.parse(key, expectedString);
+			else if (actual instanceof Integer)
+				expected = ParameterType.Int.parse(key, expectedString);
+			else
+				expected = expectedString;
+		} catch (ValidationError e) {
+			String typ = actual.getClass().getName();
+			throw new Failure("Cannot convert expected value <" + expectedString + "> to " + typ + ": " + e.getMessage());
+		}
 
-        if (actual.equals(expected))
-            return;
-        throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">");
-    }
+		if (actual.equals(expected))
+			return;
+		throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">");
+	}
 
-    private Target.Validated tryValidate() throws ValidationError {
-        if (validated == null)
-            validated = target.validate();
-        return validated;
-    }
+	private Target.Validated tryValidate() throws ValidationError {
+		if (validated == null)
+			validated = target.validate();
+		return validated;
+	}
 
-    private Object extract(String key) throws ValidationError, Failure {
-        switch (key) {
-            case "valid":
-                try {
-                    tryValidate();
-                } catch (ValidationError e) {
-                    return Boolean.FALSE;
-                }
-                return Boolean.TRUE;
+	private Object extract(String key) throws ValidationError, Failure {
+		switch (key) {
+			case "valid":
+				try {
+					tryValidate();
+				} catch (ValidationError e) {
+					return Boolean.FALSE;
+				}
+				return Boolean.TRUE;
 
-            case "connect_scan":
-                return tryValidate().connectScan();
-            case "connect_port":
-                return tryValidate().connectPort();
-            case "connect_unix":
-                return tryValidate().connectUnix();
-            case "connect_tcp":
-                return tryValidate().connectTcp();
-            case "connect_tls_verify":
-                switch (tryValidate().connectVerify()) {
-                    case None:
-                        return "";
-                    case Cert:
-                        return "cert";
-                    case Hash:
-                        return "hash";
-                    case System:
-                        return "system";
-                    default:
-                        throw new IllegalStateException("unreachable");
-                }
-            case "connect_certhash_digits":
-                return tryValidate().connectCertHashDigits();
-            case "connect_binary":
-                return tryValidate().connectBinary();
-            case "connect_clientkey":
-                return tryValidate().connectClientKey();
-            case "connect_clientcert":
-                return tryValidate().connectClientCert();
+			case "connect_scan":
+				return tryValidate().connectScan();
+			case "connect_port":
+				return tryValidate().connectPort();
+			case "connect_unix":
+				return tryValidate().connectUnix();
+			case "connect_tcp":
+				return tryValidate().connectTcp();
+			case "connect_tls_verify":
+				switch (tryValidate().connectVerify()) {
+					case None:
+						return "";
+					case Cert:
+						return "cert";
+					case Hash:
+						return "hash";
+					case System:
+						return "system";
+					default:
+						throw new IllegalStateException("unreachable");
+				}
+			case "connect_certhash_digits":
+				return tryValidate().connectCertHashDigits();
+			case "connect_binary":
+				return tryValidate().connectBinary();
+			case "connect_clientkey":
+				return tryValidate().connectClientKey();
+			case "connect_clientcert":
+				return tryValidate().connectClientCert();
 
-            default:
-                Parameter parm = Parameter.forName(key);
-                if (parm != null)
-                    return target.getObject(parm);
-                else
-                    throw new Failure("Unknown attribute: " + key);
-        }
-    }
+			default:
+				Parameter parm = Parameter.forName(key);
+				if (parm != null)
+					return target.getObject(parm);
+				else
+					throw new Failure("Unknown attribute: " + key);
+		}
+	}
 
-    public static class Failure extends Exception {
-        private String filename = null;
-        private int lineno = -1;
+	public static class Failure extends Exception {
+		private String filename = null;
+		private int lineno = -1;
 
-        public Failure(String message) {
-            super(message);
-        }
+		public Failure(String message) {
+			super(message);
+		}
 
-        @Override
-        public String getMessage() {
-            StringBuilder buffer = new StringBuilder();
-            if (filename != null) {
-                buffer.append(filename).append(":");
-                if (lineno >= 0)
-                    buffer.append(lineno).append(":");
-            }
-            buffer.append(super.getMessage());
-            return buffer.toString();
-        }
+		@Override
+		public String getMessage() {
+			StringBuilder buffer = new StringBuilder();
+			if (filename != null) {
+				buffer.append(filename).append(":");
+				if (lineno >= 0)
+					buffer.append(lineno).append(":");
+			}
+			buffer.append(super.getMessage());
+			return buffer.toString();
+		}
 
-        public String getFilename() {
-            return filename;
-        }
+		public String getFilename() {
+			return filename;
+		}
 
-        public void setFilename(String filename) {
-            this.filename = filename;
-        }
+		public void setFilename(String filename) {
+			this.filename = filename;
+		}
 
-        public int getLineno() {
-            return lineno;
-        }
+		public int getLineno() {
+			return lineno;
+		}
 
-        public void setLineno(int lineno) {
-            this.lineno = lineno;
-        }
-    }
+		public void setLineno(int lineno) {
+			this.lineno = lineno;
+		}
+	}
 }