changeset 905:a52bc2dcdb8c

Implement ClientInfo API And move ClientInfo out out MapiSocket
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Mon, 17 Jun 2024 15:54:45 +0200 (9 months ago)
parents 2d880f90be2a
children 8c8c423dc619
files src/main/java/org/monetdb/jdbc/MonetConnection.java src/main/java/org/monetdb/mcl/net/ClientInfo.java src/main/java/org/monetdb/mcl/net/MapiSocket.java src/main/java/org/monetdb/mcl/net/Target.java
diffstat 4 files changed, 183 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/src/main/java/org/monetdb/jdbc/MonetConnection.java
+++ b/src/main/java/org/monetdb/jdbc/MonetConnection.java
@@ -21,6 +21,7 @@ import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
 import java.sql.SQLClientInfoException;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
@@ -35,6 +36,7 @@ import java.util.concurrent.Executor;
 import org.monetdb.mcl.io.BufferedMCLReader;
 import org.monetdb.mcl.io.BufferedMCLWriter;
 import org.monetdb.mcl.io.LineType;
+import org.monetdb.mcl.net.ClientInfo;
 import org.monetdb.mcl.net.MapiSocket;
 import org.monetdb.mcl.net.Target;
 import org.monetdb.mcl.parser.HeaderLineParser;
@@ -118,6 +120,11 @@ public class MonetConnection
 	/** A template to apply to each command (like pre and post fixes), filled in constructor */
 	private final String[] commandTempl = new String[2]; // pre, post
 
+	/** A mapping of ClientInfo property names such as 'ClientHostname' to columns of the
+	 * sessions table, such as 'hostname'.
+	 */
+	private HashMap<String,String> clientInfoAttributeNames = null;
+
 	/** the SQL language */
 	private static final int LANG_SQL = 0;
 	/** the MAL language (officially *NOT* supported) */
@@ -224,9 +231,16 @@ public class MonetConnection
 			throw sqle;
 		}
 
-		// send any clientinfo
-		if (server.hasClientInfo()) {
-			sendControlCommand("clientinfo " + server.getClientInfo().format());
+		if (server.canClientInfo() && target.sendClientInfo()) {
+			ClientInfo info = new ClientInfo();
+			info.setDefaults();
+			String clientApplication = target.getClientApplication();
+			String clientRemark = target.getClientRemark();
+			if (!clientApplication.isEmpty())
+				info.set("ApplicationName", clientApplication);
+			if (!clientRemark.isEmpty())
+				info.set("ClientRemark", clientRemark);
+			sendClientInfo(info);
 		}
 
 		// Now take care of any options not handled during the handshake
@@ -1270,8 +1284,16 @@ public class MonetConnection
 	 */
 	@Override
 	public String getClientInfo(final String name) throws SQLException {
-		// MonetDB doesn't support any Client Info Properties yet
-		return null;
+		String attrName = getClientInfoAttributeNames().get(name);
+		if (attrName == null)
+			return null;
+		String query = "SELECT " + attrName + " FROM sys.sessions WHERE sessionid = current_sessionid()";
+		try (Statement st = createStatement(); ResultSet rs = st.executeQuery(query)) {
+			if (rs.next())
+				return rs.getString(1);
+			else
+				return null;
+		}
 	}
 
 	/**
@@ -1289,7 +1311,53 @@ public class MonetConnection
 	@Override
 	public Properties getClientInfo() throws SQLException {
 		// MonetDB doesn't support any Client Info Properties yet
-		return new Properties();
+		Properties props = new Properties();
+
+		if (server.canClientInfo()) {
+			StringBuilder builder = new StringBuilder("SELECT ");
+			String sep = "";
+			for (Entry<String, String> entry: getClientInfoAttributeNames().entrySet()) {
+				String jdbcName = entry.getKey();
+				String attrName = entry.getValue();
+				builder.append(sep);
+				sep = ", ";
+				builder.append(attrName);
+				builder.append(" AS \"");
+				builder.append(jdbcName);
+				builder.append("\"");
+			}
+			builder.append(" FROM sys.sessions WHERE sessionid = current_sessionid()");
+
+			try (
+					Statement st = createStatement();
+					ResultSet rs = st.executeQuery(builder.toString())
+			) {
+				if (rs.next()) {
+					ResultSetMetaData md = rs.getMetaData();
+					for (int i = 1; i <= md.getColumnCount(); i++) {
+						String key = md.getColumnName(i);
+						String value = rs.getString(i);
+						props.setProperty(key, value != null ? value : "");
+					}
+				}
+			}
+		}
+		return props;
+	}
+
+	private HashMap<String,String> getClientInfoAttributeNames() throws SQLException {
+		if (clientInfoAttributeNames == null) {
+			HashMap<String, String> map = new HashMap<>();
+			try (Statement st = createStatement(); ResultSet rs = st.executeQuery("SELECT prop, session_attr FROM sys.clientinfo_properties")) {
+				while (rs.next()) {
+					String jdbcName = rs.getString(1);
+					String attrName = rs.getString(2);
+					map.put(jdbcName, attrName);
+				}
+			}
+			clientInfoAttributeNames = map;
+		}
+		return clientInfoAttributeNames;
 	}
 
 	/**
@@ -1330,8 +1398,16 @@ public class MonetConnection
 	 */
 	@Override
 	public void setClientInfo(final String name, final String value) throws SQLClientInfoException {
-		// MonetDB doesn't support any Client Info Properties yet
-		addWarning("setClientInfo: client info property name not recognized", "01M07");
+		ClientInfo info = new ClientInfo();
+		try {
+			info.set(name, value, getClientInfoAttributeNames().keySet());
+			sendClientInfo(info);
+			SQLWarning warn = info.warnings();
+			if (warn != null)
+				addWarning(warn);
+		} catch (SQLException e) {
+			throw info.wrapException(e);
+		}
 	}
 
 	/**
@@ -1359,13 +1435,29 @@ public class MonetConnection
 	 */
 	@Override
 	public void setClientInfo(final Properties props) throws SQLClientInfoException {
-		if (props != null) {
-			for (Entry<Object, Object> entry : props.entrySet()) {
-				setClientInfo(entry.getKey().toString(), entry.getValue().toString());
+		ClientInfo info = new ClientInfo();
+		try {
+			for (String name: props.stringPropertyNames()) {
+				String value = props.getProperty(name);
+				info.set(name, value, getClientInfoAttributeNames().keySet());
 			}
+			sendClientInfo(info);
+			SQLWarning warn = info.warnings();
+			if (warn != null)
+				addWarning(warn);
+		} catch (SQLClientInfoException e) {
+			throw e;
+		} catch (SQLException e) {
+			throw info.wrapException(e);
 		}
 	}
 
+	private void sendClientInfo(ClientInfo info) throws SQLException {
+		String formatted = info.format();
+		if (!formatted.isEmpty())
+			sendControlCommand("clientinfo " + formatted);
+	}
+
 	//== Java 1.7 methods (JDBC 4.1)
 
 	/**
@@ -2052,16 +2144,28 @@ public class MonetConnection
 	 * warning will be the first, otherwise this warning will get
 	 * appended to the current warning.
 	 *
+	 * @param warning The warning to add
+	 */
+	private final void addWarning(SQLWarning warning) {
+		if (warnings == null) {
+			warnings = warning;
+		} else {
+			warnings.setNextWarning(warning);
+		}
+	}
+
+	/**
+	 * Adds a warning to the pile of warnings this Connection object has.
+	 * If there were no warnings (or clearWarnings was called) this
+	 * warning will be the first, otherwise this warning will get
+	 * appended to the current warning.
+	 *
 	 * @param reason the warning message
 	 * @param sqlstate the SQLState code (5 characters)
 	 */
 	private final void addWarning(final String reason, final String sqlstate) {
-		final SQLWarning warng = new SQLWarning(reason, sqlstate);
-		if (warnings == null) {
-			warnings = warng;
-		} else {
-			warnings.setNextWarning(warng);
-		}
+		final SQLWarning warning = new SQLWarning(reason, sqlstate);
+		addWarning(warning);
 	}
 
 	/** the default number of rows that are (attempted to) read at once */
--- a/src/main/java/org/monetdb/mcl/net/ClientInfo.java
+++ b/src/main/java/org/monetdb/mcl/net/ClientInfo.java
@@ -9,10 +9,17 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.sql.ClientInfoStatus;
 import java.sql.SQLClientInfoException;
-import java.util.Collections;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 
+/**
+ * Manage ClientInfo properties to track, and help generating a
+ * @{link SQLClientInfoException} if there is a failure
+ */
 public class ClientInfo {
 	private static final String defaultHostname = findHostname();
 
@@ -23,9 +30,13 @@ public class ClientInfo {
 	private static final String defaultPid = findPid();
 
 	private final Properties props;
+	private HashMap<String, ClientInfoStatus> problems = null;
 
 	public ClientInfo() {
 		props = new Properties();
+	}
+
+	public void setDefaults() {
 		props.setProperty("ClientHostname", defaultHostname);
 		props.setProperty("ClientLibrary", defaultClientLibrary);
 		props.setProperty("ClientPid", defaultPid);
@@ -88,24 +99,56 @@ public class ClientInfo {
 	}
 
 	public Properties get() {
-		Properties ret = new Properties();
-		ret.putAll(props);
-		return ret;
+		return props;
 	}
 
-	public boolean set(String name, String value) throws SQLClientInfoException {
+	public HashMap<String,ClientInfoStatus> getProblems() {
+		return problems;
+	}
+
+	public void set(String name, String value, Set<String> known) throws SQLClientInfoException {
 		if (value == null)
 			value = "";
-		if (value.contains("\n")) {
-			Map<String, ClientInfoStatus> map = Collections.singletonMap(name, ClientInfoStatus.REASON_VALUE_INVALID);
-			throw new SQLClientInfoException(map);
-		}
-		if (props.containsKey(name)) {
+
+		if (known != null && !known.contains(name)) {
+			addProblem(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
+		} else if (value.contains("\n")) {
+			addProblem(name, ClientInfoStatus.REASON_VALUE_INVALID);
+			throw new SQLClientInfoException("Invalid value for Client Info property '" + name + "'", "01M07", problems);
+		} else {
 			props.setProperty(name, value);
-			return true;
-		} else {
-			return false;
 		}
 	}
 
+	public void set(String name, String value) throws SQLClientInfoException {
+		set(name, value, null);
+	}
+
+	private void addProblem(String name, ClientInfoStatus status) {
+		if (problems == null)
+			problems = new HashMap<>();
+		ClientInfoStatus old = problems.get(name);
+		if (old == null || status.compareTo(old) > 0)
+			problems.put(name, status);
+	}
+
+	public SQLClientInfoException wrapException(SQLException e) {
+		return new SQLClientInfoException(problems, e);
+	}
+
+	public SQLWarning warnings() {
+		SQLWarning ret = null;
+		if (problems == null)
+			return null;
+		for (Map.Entry<String, ClientInfoStatus> entry: problems.entrySet()) {
+			if (!entry.getValue().equals(ClientInfoStatus.REASON_UNKNOWN_PROPERTY))
+				continue;
+			SQLWarning warning = new SQLWarning("unknown client info property: " + entry.getKey(), "01M07");
+			if (ret == null)
+				ret = warning;
+			else
+				ret.setNextWarning(warning);
+		}
+		return ret;
+	}
 }
--- a/src/main/java/org/monetdb/mcl/net/MapiSocket.java
+++ b/src/main/java/org/monetdb/mcl/net/MapiSocket.java
@@ -25,7 +25,6 @@ import java.net.*;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.sql.SQLClientInfoException;
 import java.util.*;
 
 import javax.net.ssl.SSLException;
@@ -118,7 +117,7 @@ public final class MapiSocket {
 	private BufferedMCLWriter writer;
 	/** protocol version of the connection */
 	private int version;
-	private ClientInfo clientInfo;
+	private boolean supportsClientInfo;
 
 	/** Whether we should follow redirects.
 	 * Not sure why this needs to be separate
@@ -499,20 +498,8 @@ public final class MapiSocket {
 		String optionsPart = parts.length > 6 ? parts[6] : null;
 //		String binaryPart = parts.length > 7 ? parts[7] : null;
 
-		if (parts.length > 9 && target.isClientInfo()) {
-			clientInfo = new ClientInfo();
-			try {
-				String clientApplication = target.getClientApplication();
-				String clientRemark = target.getClientRemark();
-				if (!clientApplication.isEmpty())
-					clientInfo.set("ApplicationName", clientApplication);
-				if (!clientRemark.isEmpty())
-					clientInfo.set("ClientRemark", clientRemark);
-			} catch (SQLClientInfoException e) {
-				String keys = String.join(", ", e.getFailedProperties().keySet());
-				throw new MCLException("Could not set ClientInfo properties: " + keys, e);
-			}
-		}
+		if (parts.length > 9)
+			supportsClientInfo = true;
 
 		String userResponse;
 		String password = target.getPassword();
@@ -788,15 +775,10 @@ public final class MapiSocket {
 		return target.isDebug();
 	}
 
-	public boolean hasClientInfo() {
-		return clientInfo != null;
+	public boolean canClientInfo() {
+		return supportsClientInfo;
 	}
 
-	public ClientInfo getClientInfo() {
-		if (clientInfo == null)
-			clientInfo = new ClientInfo();
-		return clientInfo;
-	}
 
 	/**
 	 * Inner class that is used to write data on a normal stream as a
--- a/src/main/java/org/monetdb/mcl/net/Target.java
+++ b/src/main/java/org/monetdb/mcl/net/Target.java
@@ -528,7 +528,7 @@ public class Target {
 		validated = null;
 	}
 
-	public boolean isClientInfo() {
+	public boolean sendClientInfo() {
 		return clientInfo;
 	}