view src/main/java/org/monetdb/mcl/net/Target.java @ 789:88c5b678e974 monetdbs

URL parser passes the tests. Some tests had to change to accomodate Java.
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Wed, 29 Nov 2023 13:28:52 +0100 (17 months ago)
parents
children 547eca89fc5e
line wrap: on
line source
package org.monetdb.mcl.net;

import java.util.Calendar;
import java.util.Properties;
import java.util.regex.Pattern;

public class Target {
    private static Pattern namePattern = Pattern.compile("^[a-zA-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 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 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 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 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 static Object getDefault(Parameter parm) {
        if (parm == Parameter.TIMEZONE) return timezone();
        else return parm.defaultValue;
    }

    public static Properties defaultProperties() {
        Properties props = new Properties();
        return props;
    }

    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;
    }

    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 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 binary;
    }

    public int getReplysize() {
        return replysize;
    }

    public String getHash() {
        return hash;
    }

    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 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 binary;
    }

    public String connectClientKey() {
        return clientkey;
    }

    public String connectClientCert() {
        return clientcert.isEmpty() ? clientkey : clientcert;
    }
}