view src/main/java/org/monetdb/mcl/net/Target.java @ 931:df18aa5c8a61

Add test for MonetDriver.getPropertyInfo(url, props). The implementation is moved to Parameter.java which contains the list of connection parameters. It currently only returns the mandatory connection parameters.
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 24 Oct 2024 19:10:06 +0200 (6 months ago)
parents 2543e24eb79a
children d416e9b6b3d0
line wrap: on
line source
/*
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2024 MonetDB Foundation;
 * Copyright August 2008 - 2023 MonetDB B.V.;
 * Copyright 1997 - July 2008 CWI.
 */

package org.monetdb.mcl.net;

import java.net.URISyntaxException;
import java.util.Properties;
import java.util.regex.Pattern;

public final class Target {
	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 clientInfo = true;
	private String clientApplication = "";
	private String clientRemark = "";
	private boolean userWasSet = false;
	private boolean passwordWasSet = false;
	private Validated validated = null;

	public Target() {
		this.timezone = (int) Parameter.TIMEZONE.getDefault();
	}

	public Target(String url, Properties props) throws URISyntaxException, ValidationError {
		this();
		setProperties(props);
		parseUrl(url);
	}

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

			case SO_TIMEOUT:
				setSoTimeout((int) value);
				break;
			case CLOB_AS_VARCHAR:
				setTreatClobAsVarchar((boolean) value);
				break;
			case BLOB_AS_BINARY:
				setTreatBlobAsBinary((boolean) value);
				break;
			case CLIENT_INFO:
				setClientInfo((boolean) value);
				break;
			case CLIENT_APPLICATION:
				setClientApplication((String) value);
				break;
			case CLIENT_REMARK:
				setClientRemark((String) value);
				break;

			default:
				throw new IllegalStateException("unreachable -- missing case: " + parm.name);
		}
	}

	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;
			case CLIENT_INFO:
				return clientInfo;
			case CLIENT_APPLICATION:
				return clientApplication;
			case CLIENT_REMARK:
				return clientRemark;
			default:
				throw new IllegalStateException("unreachable -- missing case");
		}
	}

	public boolean isTls() {
		return tls;
	}

	public void setTls(boolean tls) {
		this.tls = tls;
		validated = null;
	}

	public String getHost() {
		return host;
	}

	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 void setDatabase(String database) {
		this.database = database;
		validated = null;
	}

	public String getTableschema() {
		return tableschema;
	}

	public void setTableschema(String tableschema) {
		this.tableschema = tableschema;
		validated = null;
	}

	public String getTable() {
		return table;
	}

	public void setTable(String table) {
		this.table = table;
		validated = null;
	}

	public String getSock() {
		return sock;
	}

	public void setSock(String sock) {
		this.sock = sock;
		validated = null;
	}

	public String getSockdir() {
		return sockdir;
	}

	public void setSockdir(String sockdir) {
		this.sockdir = sockdir;
		validated = null;
	}

	public String getCert() {
		return cert;
	}

	public void setCert(String cert) {
		this.cert = cert;
		validated = null;
	}

	public String getCerthash() {
		return certhash;
	}

	public void setCerthash(String certhash) {
		this.certhash = certhash;
		validated = null;
	}

	public String getClientkey() {
		return clientkey;
	}

	public void setClientkey(String clientkey) {
		this.clientkey = clientkey;
		validated = null;
	}

	public String getClientcert() {
		return clientcert;
	}

	public void setClientcert(String clientcert) {
		this.clientcert = clientcert;
		validated = null;
	}

	public String getUser() {
		return user;
	}

	public void setUser(String user) {
		this.user = user;
		this.userWasSet = true;
		validated = null;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
		this.passwordWasSet = true;
		validated = null;
	}

	public String getLanguage() {
		return language;
	}

	public void setLanguage(String language) {
		this.language = language;
		validated = null;
	}

	public boolean isAutocommit() {
		return autocommit;
	}

	public void setAutocommit(boolean autocommit) {
		this.autocommit = autocommit;
		validated = null;
	}

	public String getSchema() {
		return schema;
	}

	public void setSchema(String schema) {
		this.schema = schema;
		validated = null;
	}

	public int getTimezone() {
		return timezone;
	}

	public void setTimezone(int timezone) {
		this.timezone = timezone;
		validated = null;
	}

	public String getBinary() {
		return binary;
	}

	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 void setHash(String hash) {
		this.hash = hash;
		validated = null;
	}

	public boolean isDebug() {
		return debug;
	}

	public void setDebug(boolean debug) {
		this.debug = debug;
		validated = null;
	}

	public String getLogfile() {
		return logfile;
	}

	public void setLogfile(String logfile) {
		this.logfile = logfile;
		validated = null;
	}

	public int getSoTimeout() {
		return soTimeout;
	}

	public void setSoTimeout(int soTimeout) {
		this.soTimeout = soTimeout;
		validated = null;
	}

	public boolean isTreatClobAsVarchar() {
		return treatClobAsVarchar;
	}

	public void setTreatClobAsVarchar(boolean treatClobAsVarchar) {
		this.treatClobAsVarchar = treatClobAsVarchar;
		validated = null;
	}

	public boolean isTreatBlobAsBinary() {
		return treatBlobAsBinary;
	}

	public void setTreatBlobAsBinary(boolean treatBlobAsBinary) {
		this.treatBlobAsBinary = treatBlobAsBinary;
		validated = null;
	}

	public boolean sendClientInfo() {
		return clientInfo;
	}

	public void setClientInfo(boolean clientInfo) {
		this.clientInfo = clientInfo;
	}

	public String getClientApplication() {
		return clientApplication;
	}

	public void setClientApplication(String clientApplication) {
		this.clientApplication = clientApplication;
	}

	public String getClientRemark() {
		return clientRemark;
	}

	public void setClientRemark(String clientRemark) {
		this.clientRemark = clientRemark;
	}

	public Validated validate() throws ValidationError {
		if (validated == null)
			validated = new Validated();
		return validated;
	}

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

		return props;
	}

	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 {

		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://");
			}

			// 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=");

			// JDBC specific
			if (soTimeout < 0)
				throw new ValidationError("so_timeout= must not be negative");
		}

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

		public int getReplySize() {
			return replySize;
		}

		public String getHash() {
			return hash;
		}

		public boolean getDebug() {
			return debug;
		}

		public String getLogfile() {
			return logfile;
		}

		public int getSoTimeout() {
			return soTimeout;
		}

		public boolean isTreatClobAsVarchar() {
			return treatClobAsVarchar;
		}

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

		public String connectClientKey() {
			return clientkey;
		}

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