view src/main/java/org/monetdb/mcl/net/MonetUrlParser.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.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * Helper class to keep the URL parsing code separate from the rest of
 * the {@link Target} class.
 */
public final class MonetUrlParser {
	private final Target target;
	private final String urlText;
	private final URI url;

	private MonetUrlParser(Target target, String url) throws URISyntaxException {
		this.target = target;
		this.urlText = url;
		// we want to accept monetdb:// but the Java URI parser rejects that.
		switch (url) {
			case "monetdb:-":
			case "monetdbs:-":
				throw new URISyntaxException(url, "invalid MonetDB URL");
			case "monetdb://":
			case "monetdbs://":
				url += "-";
				break;
		}
		this.url = new URI(url);
	}

	public static void parse(Target target, String url) throws URISyntaxException, ValidationError {
		if (url.equals("monetdb://")) {
			// deal with peculiarity of Java's URI parser
			url = "monetdb:///";
		}

		target.barrier();
		if (url.startsWith("mapi:")) {
			try {
				MonetUrlParser parser = new MonetUrlParser(target, url.substring(5));
				parser.parseClassic();
			} catch (URISyntaxException e) {
				URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1);
				exc.setStackTrace(e.getStackTrace());
				throw exc;
			}
		} else {
			MonetUrlParser parser = new MonetUrlParser(target, url);
			parser.parseModern();
		}
		target.barrier();
	}

	public static String percentDecode(String context, String text) throws URISyntaxException {
		try {
			return URLDecoder.decode(text, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e);
		} catch (IllegalArgumentException e) {
			throw new URISyntaxException(text, context + ": invalid percent escape");
		}
	}

	public static String percentEncode(String text) {
		try {
			return URLEncoder.encode(text, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("fallthrough")
	private void parseModern() throws URISyntaxException, ValidationError {
		clearBasic();

		String scheme = url.getScheme();
		if (scheme == null)
			throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
		switch (scheme) {
			case "monetdb":
				target.setTls(false);
				break;
			case "monetdbs":
				target.setTls(true);
				break;
			default:
				throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
		}

		// The built-in getHost and getPort methods do strange things
		// in edge cases such as percent-encoded host names and
		// invalid port numbers
		String authority = url.getAuthority();
		String host;
		String remainder;
		int pos;
		if (authority == null) {
			if (!url.getRawSchemeSpecificPart().startsWith("//")) {
				throw new URISyntaxException(urlText, "expected //");
			}
			host = "";
			remainder = "";
		} else if (authority.equals("-")) {
			host = "";
			remainder = "";
		} else {
			if (authority.startsWith("[")) {
				// IPv6
				pos = authority.indexOf(']');
				if (pos < 0)
					throw new URISyntaxException(urlText, "unmatched '['");
				host = authority.substring(1, pos);
				remainder = authority.substring(pos + 1);
			} else if ((pos = authority.indexOf(':')) >= 0) {
				host = authority.substring(0, pos);
				remainder = authority.substring(pos);
			} else {
				host = authority;
				remainder = "";
			}
		}
		host = Target.unpackHost(host);
		target.setHost(host);

		if (remainder.isEmpty()) {
			// do nothing
		} else if (remainder.startsWith(":")) {
			String portStr = remainder.substring(1);
			try {
				int port = Integer.parseInt(portStr);
				if (port <= 0 || port > 65535)
					portStr = null;
			} catch (NumberFormatException e) {
				portStr = null;
			}
			if (portStr == null)
				throw new ValidationError(urlText, "invalid port number");
			target.setString(Parameter.PORT, portStr);
		}

		String path = url.getRawPath();
		String[] parts = path.split("/", 4);
		// <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist>
		switch (parts.length) {
			case 4:
				target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
				// fallthrough
			case 3:
				target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
				// fallthrough
			case 2:
				target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
			case 1:
			case 0:
				// fallthrough
				break;
		}

		final String query = url.getRawQuery();
		if (query != null) {
			final String[] args = query.split("&");
			for (int i = 0; i < args.length; i++) {
				pos = args[i].indexOf('=');
				if (pos <= 0) {
					throw new URISyntaxException(args[i], "invalid key=value pair");
				}
				String key = args[i].substring(0, pos);
				key = percentDecode(key, key);
				Parameter parm = Parameter.forName(key);
				if (parm != null && parm.isCore)
					throw new URISyntaxException(key, key + "= is not allowed as a query parameter");

				String value = args[i].substring(pos + 1);
				target.setString(key, percentDecode(key, value));
			}
		}
	}

	private void parseClassic() throws URISyntaxException, ValidationError {
		if (!url.getRawSchemeSpecificPart().startsWith("//")) {
			throw new URISyntaxException(urlText, "expected //");
		}

		String scheme = url.getScheme();
		if (scheme == null)
			scheme = "";
		switch (scheme) {
			case "monetdb":
				parseClassicAuthorityAndPath();
				break;
			case "merovingian":
				String authority = url.getRawAuthority();
				// authority must be "proxy" ignore authority and path
				boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy");
				if (!valid)
					throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported");
				break;
			default:
				throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://");
		}

		final String query = url.getRawQuery();
		if (query != null) {
			final String[] args = query.split("&");
			for (int i = 0; i < args.length; i++) {
				String arg = args[i];
				if (arg.startsWith("language=")) {
					String language = arg.substring(9);
					target.setString(Parameter.LANGUAGE, language);
				} else if (arg.startsWith("database=")) {
					String database = arg.substring(9);
					target.setString(Parameter.DATABASE, database);
				} else {
					// ignore
				}
			}
		}
	}

	private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError {
		clearBasic();
		String authority = url.getRawAuthority();
		String host;
		String portStr;
		int pos;
		if (authority == null) {
			host = "";
			portStr = "";
		} else if (authority.indexOf('@') >= 0) {
			throw new URISyntaxException(urlText, "user@host syntax is not allowed");
		} else if ((pos = authority.indexOf(':')) >= 0) {
			host = authority.substring(0, pos);
			portStr = authority.substring(pos + 1);
		} else {
			host = authority;
			portStr = "";
		}

		if (!portStr.isEmpty()) {
			int port;
			try {
				port = Integer.parseInt(portStr);
			} catch (NumberFormatException e) {
				port = -1;
			}
			if (port <= 0) {
				throw new ValidationError(urlText, "invalid port number");
			}
			target.setString(Parameter.PORT, portStr);
		}

		String path = url.getRawPath();
		if (host.isEmpty() && portStr.isEmpty()) {
			// socket
			target.clear(Parameter.HOST);
			target.setString(Parameter.SOCK, path != null ? path : "");
		} else {
			// tcp
			target.clear(Parameter.SOCK);
			target.setString(Parameter.HOST, host);
			if (path == null || path.isEmpty()) {
				// do nothing
			} else if (!path.startsWith("/")) {
				throw new URISyntaxException(urlText, "expect path to start with /");
			} else {
				String database = path.substring(1);
				target.setString(Parameter.DATABASE, database);
			}
		}
	}

	private void clearBasic() {
		target.clear(Parameter.TLS);
		target.clear(Parameter.HOST);
		target.clear(Parameter.PORT);
		target.clear(Parameter.DATABASE);
	}
}