changeset 791:4de810c22328 monetdbs

Refactor
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Fri, 01 Dec 2023 14:18:01 +0100 (16 months ago)
parents 547eca89fc5e
children 9dea0795a926
files 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/Target.java tests/UrlTester.java tests/tests.md
diffstat 6 files changed, 548 insertions(+), 378 deletions(-) [+]
line wrap: on
line diff
--- a/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java
+++ b/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java
@@ -4,17 +4,14 @@ import java.io.UnsupportedEncodingExcept
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLDecoder;
-import java.util.Properties;
 
 public class MonetUrlParser {
-    private final Properties props;
+    private final Target target;
     private final String urlText;
     private final URI url;
-    boolean userWasSet = false;
-    boolean passwordWasSet = false;
 
-    public MonetUrlParser(Properties props, String url) throws URISyntaxException {
-        this.props = props;
+    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) {
@@ -29,7 +26,7 @@ public class MonetUrlParser {
         this.url = new URI(url);
     }
 
-    public static void parse(Properties props, String url) throws URISyntaxException {
+    public static void parse(Target target, String url) throws URISyntaxException, ValidationError {
         boolean modern = true;
         if (url.startsWith("mapi:")) {
             modern = false;
@@ -38,16 +35,16 @@ public class MonetUrlParser {
                 // deal with peculiarity of Java's URI parser
                 url = "monetdb:///";
             }
+        }
 
-        }
+        target.barrier();
         try {
-            MonetUrlParser parser = new MonetUrlParser(props, url);
+            MonetUrlParser parser = new MonetUrlParser(target, url);
             if (modern) {
                 parser.parseModern();
             } else {
                 parser.parseClassic();
             }
-            if (parser.userWasSet && !parser.passwordWasSet) parser.clear(Parameter.PASSWORD);
         } catch (URISyntaxException e) {
             int idx = e.getIndex();
             if (idx >= 0 && !modern) {
@@ -56,6 +53,7 @@ public class MonetUrlParser {
             }
             throw new URISyntaxException(e.getInput(), e.getReason(), idx);
         }
+        target.barrier();
     }
 
     private static String percentDecode(String context, String text) throws URISyntaxException {
@@ -68,53 +66,17 @@ public class MonetUrlParser {
         }
     }
 
-    private void set(Parameter parm, String value) {
-        parm = keyMagic(parm);
-        props.setProperty(parm.name, value != null ? value : "");
-    }
-
-    private void set(String key, String value) {
-        Parameter parm = Parameter.forName(key);
-        if (parm != null)
-            set(parm, value);
-        else
-            props.setProperty(key, value);
-    }
-
-    private void clear(Parameter parm) {
-        parm = keyMagic(parm);
-        String value = parm.type.format(Target.getDefault(parm));
-        props.setProperty(parm.name, value);
-    }
-
-    private Parameter keyMagic(Parameter key) {
-        switch (key) {
-            case USER:
-                userWasSet = true;
-                break;
-            case PASSWORD:
-                passwordWasSet = true;
-                break;
-            case FETCHSIZE:
-                key = Parameter.REPLYSIZE;
-                break;
-            default:
-                break;
-        }
-        return key;
-    }
-
-    private void parseModern() throws URISyntaxException {
+    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":
-                set(Parameter.TLS, "false");
+                target.setTls(false);
                 break;
             case "monetdbs":
-                set(Parameter.TLS, "true");
+                target.setTls(true);
                 break;
             default:
                 throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
@@ -153,8 +115,8 @@ public class MonetUrlParser {
                 remainder = "";
             }
         }
-        host = unwrapLocalhost(host);
-        set(Parameter.HOST, host);
+        host = Target.unpackHost(host);
+        target.setHost(host);
 
         if (remainder.isEmpty()) {
             // do nothing
@@ -168,24 +130,22 @@ public class MonetUrlParser {
                 portStr = null;
             }
             if (portStr == null)
-                throw new URISyntaxException(urlText, "invalid port number");
-            set(Parameter.PORT, portStr);
+                throw new ValidationError(urlText, "invalid port number");
+            target.setString(Parameter.PORT, portStr);
         }
 
         String path = url.getRawPath();
-        String[] parts = path.split("/", 5);
+        String[] parts = path.split("/", 4);
         // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist>
         switch (parts.length) {
-            case 5:
-                throw new URISyntaxException(urlText, "table name should not contain slashes");
             case 4:
-                set(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
+                target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
                 // fallthrough
             case 3:
-                set(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
+                target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
                 // fallthrough
             case 2:
-                set(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
+                target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
             case 1:
             case 0:
                 // fallthrough
@@ -207,37 +167,12 @@ public class MonetUrlParser {
                     throw new URISyntaxException(key, key + "= is not allowed as a query parameter");
 
                 String value = args[i].substring(pos + 1);
-                set(key, percentDecode(key, value));
+                target.setString(key, percentDecode(key, value));
             }
         }
     }
 
-    public static String wrapLocalhost(String host) {
-        switch (host) {
-            case "localhost":
-                host = "localhost.";
-                break;
-            case "":
-                host = "localhost";
-                break;
-        }
-        return host;
-    }
-
-    public static String unwrapLocalhost(String host) {
-        switch (host) {
-            case "localhost":
-                host = "";
-                break;
-            case "localhost.":
-                host = "localhost";
-                break;
-        }
-        return host;
-    }
-
-
-    private void parseClassic() throws URISyntaxException {
+    private void parseClassic() throws URISyntaxException, ValidationError {
         String scheme = url.getScheme();
         if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://");
         switch (scheme) {
@@ -279,9 +214,9 @@ public class MonetUrlParser {
                 port = -1;
             }
             if (port <= 0) {
-                throw new URISyntaxException(urlText, "invalid port number");
+                throw new ValidationError(urlText, "invalid port number");
             }
-            set(Parameter.PORT, portStr);
+            target.setString(Parameter.PORT, portStr);
         }
 
         String path = url.getRawPath();
@@ -289,22 +224,20 @@ public class MonetUrlParser {
         if (host.isEmpty() && portStr.isEmpty()) {
             // socket
             isUnix = true;
-            clear(Parameter.HOST);
-            set(Parameter.SOCK, path != null ? path : "");
+            target.clear(Parameter.HOST);
+            target.setString(Parameter.SOCK, path != null ? path : "");
         } else {
             // tcp
             isUnix = false;
-            clear(Parameter.SOCK);
-            set(Parameter.HOST, host);
+            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);
-                if (database.contains("/"))
-                    throw new URISyntaxException(urlText, "no slashes allowed in database name");
-                set(Parameter.DATABASE, database);
+                target.setString(Parameter.DATABASE, database);
             }
         }
 
@@ -315,10 +248,10 @@ public class MonetUrlParser {
                 String arg = args[i];
                 if (arg.startsWith("language=")) {
                     String language = arg.substring(9);
-                    set(Parameter.LANGUAGE, language);
+                    target.setString(Parameter.LANGUAGE, language);
                 } else if (arg.startsWith("database=")) {
                     String database = arg.substring(9);
-                    set(Parameter.DATABASE, database);
+                    target.setString(Parameter.DATABASE, database);
                 } else {
                     // ignore
                 }
@@ -327,9 +260,9 @@ public class MonetUrlParser {
     }
 
     private void clearBasic() {
-        clear(Parameter.HOST);
-        clear(Parameter.PORT);
-        clear(Parameter.SOCK);
-        clear(Parameter.DATABASE);
+        target.clear(Parameter.HOST);
+        target.clear(Parameter.PORT);
+        target.clear(Parameter.SOCK);
+        target.clear(Parameter.DATABASE);
     }
 }
--- a/src/main/java/org/monetdb/mcl/net/Parameter.java
+++ b/src/main/java/org/monetdb/mcl/net/Parameter.java
@@ -1,6 +1,8 @@
 package org.monetdb.mcl.net;
 
 
+import java.util.Calendar;
+
 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),
@@ -17,7 +19,7 @@ public enum Parameter {
     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, false, "initial value of autocommit", 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),
@@ -31,7 +33,7 @@ public enum Parameter {
 
     public final String name;
     public final ParameterType type;
-    public final Object defaultValue;
+    private final Object defaultValue;
     public final String description;
     public final boolean isCore;
 
@@ -78,4 +80,16 @@ public enum Parameter {
             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;
+        }
+    }
 }
--- a/src/main/java/org/monetdb/mcl/net/ParameterType.java
+++ b/src/main/java/org/monetdb/mcl/net/ParameterType.java
@@ -8,7 +8,7 @@ public enum ParameterType {
 
     public Object parse(String name, String value) throws ValidationError {
         if (value == null)
-            throw new NullPointerException();
+            return null;
 
         try {
             switch (this) {
--- a/src/main/java/org/monetdb/mcl/net/Target.java
+++ b/src/main/java/org/monetdb/mcl/net/Target.java
@@ -5,326 +5,570 @@ 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 = 200;
+    private String hash = "";
+    private boolean debug = false;
+    private String logfile = "";
+
+    private boolean userWasSet = false;
+    private boolean passwordWasSet = false;
+    protected static final Target defaults = new Target();
+
     private static Pattern namePattern = Pattern.compile("^[a-zzA-Z_][-a-zA-Z0-9_.]*$");
     private static Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$");
-    private final boolean tls;
-    private final String host;
-    private final int port;
-    private final String database;
-    private final String tableschema;
-    private final String table;
-    private final String sock;
-    private final String sockdir;
-    private final String cert;
-    private final String certhash;
-    private final String clientkey;
-    private final String clientcert;
-    private final String user;
-    private final String password;
-    private final String language;
-    private final boolean autocommit;
-    private final String schema;
-    private final int timezone;
-    private final int binary;
-    private final int replysize;
-    private final String hash;
-    private final boolean debug;
-    private final String logfile;
 
-    public Target(Properties properties) throws ValidationError {
-
-        // 1. The parameters have the types listed in the table in [Section
-        //    Parameters](#parameters).
-        tls = validateBoolean(properties, Parameter.TLS);
-        host = validateString(properties, Parameter.HOST);
-        port = validateInt(properties, Parameter.PORT);
-        database = validateString(properties, Parameter.DATABASE);
-        tableschema = validateString(properties, Parameter.TABLESCHEMA);
-        table = validateString(properties, Parameter.TABLE);
-        sock = validateString(properties, Parameter.SOCK);
-        sockdir = validateString(properties, Parameter.SOCKDIR);
-        cert = validateString(properties, Parameter.CERT);
-        certhash = validateString(properties, Parameter.CERTHASH);
-        clientkey = validateString(properties, Parameter.CLIENTKEY);
-        clientcert = validateString(properties, Parameter.CLIENTCERT);
-        user = validateString(properties, Parameter.USER);
-        password = validateString(properties, Parameter.PASSWORD);
-        language = validateString(properties, Parameter.LANGUAGE);
-        autocommit = validateBoolean(properties, Parameter.AUTOCOMMIT);
-        schema = validateString(properties, Parameter.SCHEMA);
-        timezone = validateInt(properties, Parameter.TIMEZONE);
-        replysize = validateInt(properties, Parameter.REPLYSIZE);
-        hash = validateString(properties, Parameter.HASH);
-        debug = validateBoolean(properties, Parameter.DEBUG);
-        logfile = validateString(properties, Parameter.LOGFILE);
-
-        for (String name: properties.stringPropertyNames()) {
-            if (Parameter.forName(name) != null)
-                continue;
-            if (name.contains("_"))
-                continue;
-            throw new ValidationError("unknown parameter: " + name);
-        }
-
-        String binaryString = validateString(properties, Parameter.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");
-        binary = 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);
-
-        // 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");
-
-
-        // 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=");
+    public Target() {
+        this.timezone = (int)Parameter.TIMEZONE.getDefault();
     }
 
-    public static boolean validateBoolean(Properties props, Parameter parm) throws ValidationError {
-        Object value = props.get(parm.name);
-        if (value != null) {
-            return (Boolean) parm.type.parse(parm.name, (String) value);
-        } else {
-            return (Boolean) getDefault(parm);
+    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 int validateInt(Properties props, Parameter parm) throws ValidationError {
-        Object value = props.get(parm.name);
-        if (value != null) {
-            return (Integer) parm.type.parse(parm.name, (String) value);
-        } else {
-            return (Integer) getDefault(parm);
-        }
+    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 clear(Parameter parm) {
+        assign(parm, parm.getDefault());
     }
 
-    public static String validateString(Properties props, Parameter parm) throws ValidationError {
-        Object value = props.get(parm.name);
-        if (value != null) {
-            return (String) parm.type.parse(parm.name, (String) value);
-        } else {
-            return (String) getDefault(parm);
+    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;
+            default:
+                throw new IllegalStateException("unreachable -- missing case");
         }
     }
 
-    private static int timezone() {
-        Calendar cal = Calendar.getInstance();
-        int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
-        int offsetSeconds = offsetMillis / 1000;
-        return offsetSeconds;
+    public String getString(Parameter parm) {
+        Object value = getObject(parm);
+        return parm.type.format(value);
     }
 
-    public static Object getDefault(Parameter parm) {
-        if (parm == Parameter.TIMEZONE) return timezone();
-        else return parm.defaultValue;
+    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;
+            default:
+                throw new IllegalStateException("unreachable -- missing case");
+        }
     }
 
-    public static Properties defaultProperties() {
-        Properties props = new Properties();
-        return props;
+    public static String unpackHost(String host) {
+        switch (host) {
+            case "localhost.":
+                return "localhost";
+            case "localhost":
+                return "";
+            default:
+                return host;
+        }
     }
 
-    public boolean getTls() {
+    public boolean isTls() {
         return tls;
     }
 
-    // Getter is private because you probably want connectTcp() instead
-    private String getHost() {
+    public void setTls(boolean tls) {
+        this.tls = tls;
+    }
+
+    public String getHost() {
         return host;
     }
 
-    // Getter is private because you probably want connectPort() instead
-    private int getPort() {
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public int getPort() {
         return port;
     }
 
+    public void setPort(int port) {
+        this.port = port;
+    }
+
     public String getDatabase() {
         return database;
     }
 
+    public void setDatabase(String database) {
+        this.database = database;
+    }
+
     public String getTableschema() {
         return tableschema;
     }
 
+    public void setTableschema(String tableschema) {
+        this.tableschema = tableschema;
+    }
+
     public String getTable() {
         return table;
     }
 
-    // Getter is private because you probably want connectUnix() instead
-    private String getSock() {
+    public void setTable(String table) {
+        this.table = table;
+    }
+
+    public String getSock() {
         return sock;
     }
 
+    public void setSock(String sock) {
+        this.sock = sock;
+    }
+
     public String getSockdir() {
         return sockdir;
     }
 
+    public void setSockdir(String sockdir) {
+        this.sockdir = sockdir;
+    }
+
     public String getCert() {
         return cert;
     }
 
+    public void setCert(String cert) {
+        this.cert = cert;
+    }
+
     public String getCerthash() {
         return certhash;
     }
 
+    public void setCerthash(String certhash) {
+        this.certhash = certhash;
+    }
+
     public String getClientkey() {
         return clientkey;
     }
 
+    public void setClientkey(String clientkey) {
+        this.clientkey = clientkey;
+    }
+
     public String getClientcert() {
         return clientcert;
     }
 
+    public void setClientcert(String clientcert) {
+        this.clientcert = clientcert;
+    }
+
     public String getUser() {
         return user;
     }
 
+    public void setUser(String user) {
+        this.user = user;
+        this.userWasSet = true;
+    }
+
     public String getPassword() {
         return password;
     }
 
+    public void setPassword(String password) {
+        this.password = password;
+        this.passwordWasSet = true;
+    }
+
     public String getLanguage() {
         return language;
     }
 
-    public boolean getAutocommit() {
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public boolean isAutocommit() {
         return autocommit;
     }
 
+    public void setAutocommit(boolean autocommit) {
+        this.autocommit = autocommit;
+    }
+
     public String getSchema() {
         return schema;
     }
 
+    public void setSchema(String schema) {
+        this.schema = schema;
+    }
+
     public int getTimezone() {
         return timezone;
     }
 
-    // Getter is private because you probably want connectBinary() instead
-    public int getBinary() {
+    public void setTimezone(int timezone) {
+        this.timezone = timezone;
+    }
+
+    public String getBinary() {
         return binary;
     }
 
+    public void setBinary(String binary) {
+        this.binary = binary;
+    }
+
     public int getReplysize() {
         return replysize;
     }
 
+    public void setReplysize(int replysize) {
+        this.replysize = replysize;
+    }
+
     public String getHash() {
         return hash;
     }
 
-    public boolean getDebug() {
+    public void setHash(String hash) {
+        this.hash = hash;
+    }
+
+    public boolean isDebug() {
         return debug;
     }
 
+    public void setDebug(boolean debug) {
+        this.debug = debug;
+    }
+
     public String getLogfile() {
         return logfile;
     }
 
-    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 void setLogfile(String logfile) {
+        this.logfile = logfile;
     }
 
-    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 Validated validate() throws ValidationError {
+        return new Validated();
     }
 
-    public Verify connectVerify() {
-        if (!tls) return Verify.None;
-        if (!certhash.isEmpty()) return Verify.Hash;
-        if (!cert.isEmpty()) return Verify.Cert;
-        return Verify.System;
-    }
+    public class Validated {
+
+        private final int nbinary;
+
+        Validated() throws ValidationError {
+
+            // 1. The parameters have the types listed in the table in [Section
+            //    Parameters](#parameters).
+
+            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);
+
+            // 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://");
+            }
 
-    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));
+            // 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);
+
+            // 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=");
+        }
+
+        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 connectPort() instead
+        private int getPort() {
+            return port;
+        }
+
+        public String getDatabase() {
+            return database;
+        }
+
+        public String getTableschema() {
+            return tableschema;
+        }
+
+        public String getTable() {
+            return table;
+        }
+
+        // Getter is private because you probably want connectUnix() instead
+        private String getSock() {
+            return sock;
+        }
+
+        public String getSockdir() {
+            return sockdir;
         }
-        return builder.toString();
-    }
+
+        public String getCert() {
+            return cert;
+        }
+
+        public String getCerthash() {
+            return certhash;
+        }
+
+        public String getClientkey() {
+            return clientkey;
+        }
+
+        public String getClientcert() {
+            return clientcert;
+        }
+
+        public String getUser() {
+            return user;
+        }
+
+        public String getPassword() {
+            return password;
+        }
 
-    public int connectBinary() {
-        return binary;
-    }
+        public String getLanguage() {
+            return language;
+        }
+
+        public boolean getAutocommit() {
+            return autocommit;
+        }
+
+        public String getSchema() {
+            return schema;
+        }
+
+        public int getTimezone() {
+            return timezone;
+        }
+
+        // Getter is private because you probably want connectBinary() instead
+        public int getBinary() {
+            return nbinary;
+        }
+
+        public int getReplysize() {
+            return replysize;
+        }
+
+        public String getHash() {
+            return hash;
+        }
 
-    public String connectClientKey() {
-        return clientkey;
-    }
+        public boolean getDebug() {
+            return debug;
+        }
+
+        public String getLogfile() {
+            return logfile;
+        }
+
+        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 String connectUnix() {
+            if (!sock.isEmpty()) return sock;
+            if (tls) return "";
+            if (host.isEmpty()) return sockdir + "/.s.monetdb." + connectPort();
+            return "";
+        }
 
-    public String connectClientCert() {
-        return clientcert.isEmpty() ? clientkey : clientcert;
+        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 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 String connectClientKey() {
+            return clientkey;
+        }
+
+        public String connectClientCert() {
+            return clientcert.isEmpty() ? clientkey : clientcert;
+        }
     }
 }
--- a/tests/UrlTester.java
+++ b/tests/UrlTester.java
@@ -2,7 +2,6 @@ import org.monetdb.mcl.net.*;
 
 import java.io.*;
 import java.net.URISyntaxException;
-import java.util.Properties;
 
 public class UrlTester {
     String filename = null;
@@ -10,8 +9,8 @@ public class UrlTester {
     BufferedReader reader = null;
     int lineno = 0;
     int testCount = 0;
-    Properties props = null;
-    Target validated = null;
+    Target target = null;
+    Target.Validated validated = null;
 
     public UrlTester() {
     }
@@ -21,6 +20,8 @@ public class UrlTester {
     }
 
     public static void main(String[] args) throws Exception {
+        checkDefaults();
+
         int exitcode;
         UrlTester tester = new UrlTester();
         exitcode = tester.parseArgs(args);
@@ -29,6 +30,21 @@ public class UrlTester {
         System.exit(exitcode);
     }
 
+    private static void checkDefaults() {
+        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)) {
+                System.err.println("Default for " + parm.name + " expected to be <" + expected + "> but is <" + actual + ">");
+                System.exit(1);
+            }
+        }
+    }
+
     private int parseArgs(String[] args) {
         for (int i = 0; i < args.length; i++) {
             String arg = args[i];
@@ -107,18 +123,18 @@ public class UrlTester {
 
     private void processLine(String line) throws Failure {
         line = line.replaceFirst("\\s+$", ""); // remove trailing
-        if (props == null && line.equals("```test")) {
+        if (target == null && line.equals("```test")) {
             if (verbose >= 2) {
                 if (testCount > 0) {
                     System.out.println();
                 }
                 System.out.println("\u25B6 " + filename + ":" + lineno);
             }
-            props = Target.defaultProperties();
+            target = new Target();
             testCount++;
             return;
         }
-        if (props != null) {
+        if (target != null) {
             if (line.equals("```")) {
                 stopProcessing();
                 return;
@@ -128,7 +144,7 @@ public class UrlTester {
     }
 
     private void stopProcessing() {
-        props = null;
+        target = null;
         validated = null;
     }
 
@@ -205,7 +221,11 @@ public class UrlTester {
         String key = splitKey(rest);
         String value = splitValue(rest);
 
-        props.put(key, value);
+        try {
+            target.setString(key, value);
+        } catch (ValidationError e) {
+            throw new Failure(e.getMessage());
+        }
     }
 
     private void handleParse(String rest, Boolean shouldSucceed) throws Failure {
@@ -214,14 +234,17 @@ public class UrlTester {
 
         validated = null;
         try {
-            MonetUrlParser.parse(props, rest);
+            target.barrier();
+            MonetUrlParser.parse(target, rest);
         } catch (URISyntaxException e) {
             parseError = e;
+        } catch (ValidationError e) {
+            validationError = e;
         }
 
-        if (parseError == null) {
+        if (parseError == null && validationError == null) {
             try {
-                validated = new Target(props);
+                tryValidate();
             } catch (ValidationError e) {
                 validationError = e;
             }
@@ -269,63 +292,14 @@ public class UrlTester {
         throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">");
     }
 
-    private Target tryValidate() throws ValidationError {
+    private Target.Validated tryValidate() throws ValidationError {
         if (validated == null)
-            validated = new Target(props);
+            validated = target.validate();
         return validated;
     }
 
     private Object extract(String key) throws ValidationError, Failure {
         switch (key) {
-            case "tls":
-                return Target.validateBoolean(props, Parameter.TLS);
-            case "host":
-                return Target.validateString(props, Parameter.HOST);
-            case "port":
-                return Target.validateInt(props, Parameter.PORT);
-            case "database":
-                return Target.validateString(props, Parameter.DATABASE);
-            case "tableschema":
-                return Target.validateString(props, Parameter.TABLESCHEMA);
-            case "table":
-                return Target.validateString(props, Parameter.TABLE);
-            case "sock":
-                return Target.validateString(props, Parameter.SOCK);
-            case "sockdir":
-                return Target.validateString(props, Parameter.SOCKDIR);
-            case "cert":
-                return Target.validateString(props, Parameter.CERT);
-            case "certhash":
-                return Target.validateString(props, Parameter.CERTHASH);
-            case "clientkey":
-                return Target.validateString(props, Parameter.CLIENTKEY);
-            case "clientcert":
-                return Target.validateString(props, Parameter.CLIENTCERT);
-            case "user":
-                return Target.validateString(props, Parameter.USER);
-            case "password":
-                return Target.validateString(props, Parameter.PASSWORD);
-            case "language":
-                return Target.validateString(props, Parameter.LANGUAGE);
-            case "autocommit":
-                return Target.validateBoolean(props, Parameter.AUTOCOMMIT);
-            case "schema":
-                return Target.validateString(props, Parameter.SCHEMA);
-            case "timezone":
-                return Target.validateInt(props, Parameter.TIMEZONE);
-            case "binary":
-                return Target.validateString(props, Parameter.BINARY);
-            case "replysize":
-                return Target.validateInt(props, Parameter.REPLYSIZE);
-            case "fetchsize":
-                return Target.validateInt(props, Parameter.FETCHSIZE);
-            case "hash":
-                return Target.validateString(props, Parameter.HASH);
-            case "debug":
-                return Target.validateBoolean(props, Parameter.DEBUG);
-            case "logfile":
-                return Target.validateString(props, Parameter.LOGFILE);
-
             case "valid":
                 try {
                     tryValidate();
@@ -361,7 +335,11 @@ public class UrlTester {
                 return tryValidate().connectClientCert();
 
             default:
-                throw new Failure("Unknown attribute: " + key);
+                Parameter parm = Parameter.forName(key);
+                if (parm != null)
+                    return target.getObject(parm);
+                else
+                    throw new Failure("Unknown attribute: " + key);
         }
     }
 
--- a/tests/tests.md
+++ b/tests/tests.md
@@ -598,14 +598,11 @@ EXPECT replysize=600
 ```
 
 ```test
-NOT jdbc
 SET replysize=200
 SET fetchsize=300
 EXPECT replysize=300
 ```
 
-
-
 Rule: parsing a URL sets all of tls, host, port and database
 even if left out of the URL
 
@@ -780,6 +777,10 @@ EXPECT connect_tcp=::1
 EXPECT database=foo
 ```
 
+```test
+REJECT monetdb://[::1]banana/foo
+```
+
 Bad percent escapes:
 
 ```test