comparison src/main/java/org/monetdb/mcl/net/MonetUrlParser.java @ 834:5aa19bbed0d6 monetdbs

Comments and formatting
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Wed, 13 Dec 2023 15:39:47 +0100 (17 months ago)
parents 6b7778153d23
children 9d21c6e7ed26
comparison
equal deleted inserted replaced
813:a71afa48f269 834:5aa19bbed0d6
4 import java.net.URI; 4 import java.net.URI;
5 import java.net.URISyntaxException; 5 import java.net.URISyntaxException;
6 import java.net.URLDecoder; 6 import java.net.URLDecoder;
7 import java.net.URLEncoder; 7 import java.net.URLEncoder;
8 8
9 /**
10 * Helper class to keep the URL parsing code separate from the rest of
11 * the {@link Target} class.
12 */
9 public class MonetUrlParser { 13 public class MonetUrlParser {
10 private final Target target; 14 private final Target target;
11 private final String urlText; 15 private final String urlText;
12 private final URI url; 16 private final URI url;
13 17
14 public MonetUrlParser(Target target, String url) throws URISyntaxException { 18 private MonetUrlParser(Target target, String url) throws URISyntaxException {
15 this.target = target; 19 this.target = target;
16 this.urlText = url; 20 this.urlText = url;
17 // we want to accept monetdb:// but the Java URI parser rejects that. 21 // we want to accept monetdb:// but the Java URI parser rejects that.
18 switch (url) { 22 switch (url) {
19 case "monetdb:-": 23 case "monetdb:-":
20 case "monetdbs:-": 24 case "monetdbs:-":
21 throw new URISyntaxException(url, "invalid MonetDB URL"); 25 throw new URISyntaxException(url, "invalid MonetDB URL");
22 case "monetdb://": 26 case "monetdb://":
23 case "monetdbs://": 27 case "monetdbs://":
24 url += "-"; 28 url += "-";
25 break; 29 break;
26 } 30 }
27 this.url = new URI(url); 31 this.url = new URI(url);
28 } 32 }
29 33
30 public static void parse(Target target, String url) throws URISyntaxException, ValidationError { 34 public static void parse(Target target, String url) throws URISyntaxException, ValidationError {
31 if (url.equals("monetdb://")) { 35 if (url.equals("monetdb://")) {
32 // deal with peculiarity of Java's URI parser 36 // deal with peculiarity of Java's URI parser
33 url = "monetdb:///"; 37 url = "monetdb:///";
34 } 38 }
35 39
36 target.barrier(); 40 target.barrier();
37 if (url.startsWith("mapi:")) { 41 if (url.startsWith("mapi:")) {
38 try { 42 try {
39 MonetUrlParser parser = new MonetUrlParser(target, url.substring(5)); 43 MonetUrlParser parser = new MonetUrlParser(target, url.substring(5));
40 parser.parseClassic(); 44 parser.parseClassic();
41 } catch (URISyntaxException e) { 45 } catch (URISyntaxException e) {
42 URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1); 46 URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1);
43 exc.setStackTrace(e.getStackTrace()); 47 exc.setStackTrace(e.getStackTrace());
44 throw exc; 48 throw exc;
45 } 49 }
46 } else { 50 } else {
47 MonetUrlParser parser = new MonetUrlParser(target, url); 51 MonetUrlParser parser = new MonetUrlParser(target, url);
48 parser.parseModern(); 52 parser.parseModern();
49 } 53 }
50 target.barrier(); 54 target.barrier();
51 } 55 }
52 56
53 public static String percentDecode(String context, String text) throws URISyntaxException { 57 public static String percentDecode(String context, String text) throws URISyntaxException {
54 try { 58 try {
55 return URLDecoder.decode(text, "UTF-8"); 59 return URLDecoder.decode(text, "UTF-8");
56 } catch (UnsupportedEncodingException e) { 60 } catch (UnsupportedEncodingException e) {
57 throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e); 61 throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e);
58 } catch (IllegalArgumentException e) { 62 } catch (IllegalArgumentException e) {
59 throw new URISyntaxException(text, context + ": invalid percent escape"); 63 throw new URISyntaxException(text, context + ": invalid percent escape");
60 } 64 }
61 } 65 }
62 66
63 public static String percentEncode(String text) { 67 public static String percentEncode(String text) {
64 try { 68 try {
65 return URLEncoder.encode(text, "UTF-8"); 69 return URLEncoder.encode(text, "UTF-8");
66 } catch (UnsupportedEncodingException e) { 70 } catch (UnsupportedEncodingException e) {
67 throw new RuntimeException(e); 71 throw new RuntimeException(e);
68 } 72 }
69 } 73 }
70 74
71 private void parseModern() throws URISyntaxException, ValidationError { 75 private void parseModern() throws URISyntaxException, ValidationError {
72 clearBasic(); 76 clearBasic();
73 77
74 String scheme = url.getScheme(); 78 String scheme = url.getScheme();
75 if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); 79 if (scheme == null)
76 switch (scheme) { 80 throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
77 case "monetdb": 81 switch (scheme) {
78 target.setTls(false); 82 case "monetdb":
79 break; 83 target.setTls(false);
80 case "monetdbs": 84 break;
81 target.setTls(true); 85 case "monetdbs":
82 break; 86 target.setTls(true);
83 default: 87 break;
84 throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); 88 default:
85 } 89 throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://");
86 90 }
87 // The built-in getHost and getPort methods do strange things 91
88 // in edge cases such as percent-encoded host names and 92 // The built-in getHost and getPort methods do strange things
89 // invalid port numbers 93 // in edge cases such as percent-encoded host names and
90 String authority = url.getAuthority(); 94 // invalid port numbers
91 String host; 95 String authority = url.getAuthority();
92 String remainder; 96 String host;
93 int pos; 97 String remainder;
94 if (authority == null) { 98 int pos;
95 if (!url.getRawSchemeSpecificPart().startsWith("//")) { 99 if (authority == null) {
96 throw new URISyntaxException(urlText, "expected //"); 100 if (!url.getRawSchemeSpecificPart().startsWith("//")) {
97 } 101 throw new URISyntaxException(urlText, "expected //");
98 host = ""; 102 }
99 remainder = ""; 103 host = "";
100 } else if (authority.equals("-")) { 104 remainder = "";
101 host = ""; 105 } else if (authority.equals("-")) {
102 remainder = ""; 106 host = "";
103 } else { 107 remainder = "";
104 if (authority.startsWith("[")) { 108 } else {
105 // IPv6 109 if (authority.startsWith("[")) {
106 pos = authority.indexOf(']'); 110 // IPv6
107 if (pos < 0) 111 pos = authority.indexOf(']');
108 throw new URISyntaxException(urlText, "unmatched '['"); 112 if (pos < 0)
109 host = authority.substring(1, pos); 113 throw new URISyntaxException(urlText, "unmatched '['");
110 remainder = authority.substring(pos + 1); 114 host = authority.substring(1, pos);
111 } else if ((pos = authority.indexOf(':')) >= 0) { 115 remainder = authority.substring(pos + 1);
112 host = authority.substring(0, pos); 116 } else if ((pos = authority.indexOf(':')) >= 0) {
113 remainder = authority.substring(pos); 117 host = authority.substring(0, pos);
114 } else { 118 remainder = authority.substring(pos);
115 host = authority; 119 } else {
116 remainder = ""; 120 host = authority;
117 } 121 remainder = "";
118 } 122 }
119 host = Target.unpackHost(host); 123 }
120 target.setHost(host); 124 host = Target.unpackHost(host);
121 125 target.setHost(host);
122 if (remainder.isEmpty()) { 126
123 // do nothing 127 if (remainder.isEmpty()) {
124 } else if (remainder.startsWith(":")) { 128 // do nothing
125 String portStr = remainder.substring(1); 129 } else if (remainder.startsWith(":")) {
126 try { 130 String portStr = remainder.substring(1);
127 int port = Integer.parseInt(portStr); 131 try {
128 if (port <= 0 || port > 65535) 132 int port = Integer.parseInt(portStr);
129 portStr = null; 133 if (port <= 0 || port > 65535)
130 } catch (NumberFormatException e) { 134 portStr = null;
131 portStr = null; 135 } catch (NumberFormatException e) {
132 } 136 portStr = null;
133 if (portStr == null) 137 }
134 throw new ValidationError(urlText, "invalid port number"); 138 if (portStr == null)
135 target.setString(Parameter.PORT, portStr); 139 throw new ValidationError(urlText, "invalid port number");
136 } 140 target.setString(Parameter.PORT, portStr);
137 141 }
138 String path = url.getRawPath(); 142
139 String[] parts = path.split("/", 4); 143 String path = url.getRawPath();
140 // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist> 144 String[] parts = path.split("/", 4);
141 switch (parts.length) { 145 // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist>
142 case 4: 146 switch (parts.length) {
143 target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3])); 147 case 4:
144 // fallthrough 148 target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3]));
145 case 3: 149 // fallthrough
146 target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2])); 150 case 3:
147 // fallthrough 151 target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2]));
148 case 2: 152 // fallthrough
149 target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1])); 153 case 2:
150 case 1: 154 target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1]));
151 case 0: 155 case 1:
152 // fallthrough 156 case 0:
153 break; 157 // fallthrough
154 } 158 break;
155 159 }
156 final String query = url.getRawQuery(); 160
157 if (query != null) { 161 final String query = url.getRawQuery();
158 final String args[] = query.split("&"); 162 if (query != null) {
159 for (int i = 0; i < args.length; i++) { 163 final String[] args = query.split("&");
160 pos = args[i].indexOf('='); 164 for (int i = 0; i < args.length; i++) {
161 if (pos <= 0) { 165 pos = args[i].indexOf('=');
162 throw new URISyntaxException(args[i], "invalid key=value pair"); 166 if (pos <= 0) {
163 } 167 throw new URISyntaxException(args[i], "invalid key=value pair");
164 String key = args[i].substring(0, pos); 168 }
165 key = percentDecode(key, key); 169 String key = args[i].substring(0, pos);
166 Parameter parm = Parameter.forName(key); 170 key = percentDecode(key, key);
167 if (parm != null && parm.isCore) 171 Parameter parm = Parameter.forName(key);
168 throw new URISyntaxException(key, key + "= is not allowed as a query parameter"); 172 if (parm != null && parm.isCore)
169 173 throw new URISyntaxException(key, key + "= is not allowed as a query parameter");
170 String value = args[i].substring(pos + 1); 174
171 target.setString(key, percentDecode(key, value)); 175 String value = args[i].substring(pos + 1);
172 } 176 target.setString(key, percentDecode(key, value));
173 } 177 }
174 } 178 }
175 179 }
176 private void parseClassic() throws URISyntaxException, ValidationError { 180
177 if (!url.getRawSchemeSpecificPart().startsWith("//")) { 181 private void parseClassic() throws URISyntaxException, ValidationError {
178 throw new URISyntaxException(urlText, "expected //"); 182 if (!url.getRawSchemeSpecificPart().startsWith("//")) {
179 } 183 throw new URISyntaxException(urlText, "expected //");
180 184 }
181 String scheme = url.getScheme(); 185
182 if (scheme == null) 186 String scheme = url.getScheme();
183 scheme = ""; 187 if (scheme == null)
184 switch (scheme) { 188 scheme = "";
185 case "monetdb": 189 switch (scheme) {
186 parseClassicAuthorityAndPath(); 190 case "monetdb":
187 break; 191 parseClassicAuthorityAndPath();
188 case "merovingian": 192 break;
189 String authority = url.getRawAuthority(); 193 case "merovingian":
190 // authority must be "proxy" ignore authority and path 194 String authority = url.getRawAuthority();
191 boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy"); 195 // authority must be "proxy" ignore authority and path
192 if (!valid) 196 boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy");
193 throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported"); 197 if (!valid)
194 break; 198 throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported");
195 default: 199 break;
196 throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); 200 default:
197 } 201 throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://");
198 202 }
199 final String query = url.getRawQuery(); 203
200 if (query != null) { 204 final String query = url.getRawQuery();
201 final String args[] = query.split("&"); 205 if (query != null) {
202 for (int i = 0; i < args.length; i++) { 206 final String[] args = query.split("&");
203 String arg = args[i]; 207 for (int i = 0; i < args.length; i++) {
204 if (arg.startsWith("language=")) { 208 String arg = args[i];
205 String language = arg.substring(9); 209 if (arg.startsWith("language=")) {
206 target.setString(Parameter.LANGUAGE, language); 210 String language = arg.substring(9);
207 } else if (arg.startsWith("database=")) { 211 target.setString(Parameter.LANGUAGE, language);
208 String database = arg.substring(9); 212 } else if (arg.startsWith("database=")) {
209 target.setString(Parameter.DATABASE, database); 213 String database = arg.substring(9);
210 } else { 214 target.setString(Parameter.DATABASE, database);
211 // ignore 215 } else {
212 } 216 // ignore
213 } 217 }
214 } 218 }
215 } 219 }
216 220 }
217 private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError { 221
218 clearBasic(); 222 private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError {
219 String authority = url.getRawAuthority(); 223 clearBasic();
220 String host; 224 String authority = url.getRawAuthority();
221 String portStr; 225 String host;
222 int pos; 226 String portStr;
223 if (authority == null) { 227 int pos;
224 host = ""; 228 if (authority == null) {
225 portStr = ""; 229 host = "";
226 } else if (authority.indexOf('@') >= 0) { 230 portStr = "";
227 throw new URISyntaxException(urlText, "user@host syntax is not allowed"); 231 } else if (authority.indexOf('@') >= 0) {
228 } else if ((pos = authority.indexOf(':')) >= 0) { 232 throw new URISyntaxException(urlText, "user@host syntax is not allowed");
229 host = authority.substring(0, pos); 233 } else if ((pos = authority.indexOf(':')) >= 0) {
230 portStr = authority.substring(pos + 1); 234 host = authority.substring(0, pos);
231 } else { 235 portStr = authority.substring(pos + 1);
232 host = authority; 236 } else {
233 portStr = ""; 237 host = authority;
234 } 238 portStr = "";
235 239 }
236 if (!portStr.isEmpty()) { 240
237 int port; 241 if (!portStr.isEmpty()) {
238 try { 242 int port;
239 port = Integer.parseInt(portStr); 243 try {
240 } catch (NumberFormatException e) { 244 port = Integer.parseInt(portStr);
241 port = -1; 245 } catch (NumberFormatException e) {
242 } 246 port = -1;
243 if (port <= 0) { 247 }
244 throw new ValidationError(urlText, "invalid port number"); 248 if (port <= 0) {
245 } 249 throw new ValidationError(urlText, "invalid port number");
246 target.setString(Parameter.PORT, portStr); 250 }
247 } 251 target.setString(Parameter.PORT, portStr);
248 252 }
249 String path = url.getRawPath(); 253
250 if (host.isEmpty() && portStr.isEmpty()) { 254 String path = url.getRawPath();
251 // socket 255 if (host.isEmpty() && portStr.isEmpty()) {
252 target.clear(Parameter.HOST); 256 // socket
253 target.setString(Parameter.SOCK, path != null ? path : ""); 257 target.clear(Parameter.HOST);
254 } else { 258 target.setString(Parameter.SOCK, path != null ? path : "");
255 // tcp 259 } else {
256 target.clear(Parameter.SOCK); 260 // tcp
257 target.setString(Parameter.HOST, host); 261 target.clear(Parameter.SOCK);
258 if (path == null || path.isEmpty()) { 262 target.setString(Parameter.HOST, host);
259 // do nothing 263 if (path == null || path.isEmpty()) {
260 } else if (!path.startsWith("/")) { 264 // do nothing
261 throw new URISyntaxException(urlText, "expect path to start with /"); 265 } else if (!path.startsWith("/")) {
262 } else { 266 throw new URISyntaxException(urlText, "expect path to start with /");
263 String database = path.substring(1); 267 } else {
264 target.setString(Parameter.DATABASE, database); 268 String database = path.substring(1);
265 } 269 target.setString(Parameter.DATABASE, database);
266 } 270 }
267 } 271 }
268 272 }
269 private void clearBasic() { 273
270 target.clear(Parameter.TLS); 274 private void clearBasic() {
271 target.clear(Parameter.HOST); 275 target.clear(Parameter.TLS);
272 target.clear(Parameter.PORT); 276 target.clear(Parameter.HOST);
273 target.clear(Parameter.DATABASE); 277 target.clear(Parameter.PORT);
274 } 278 target.clear(Parameter.DATABASE);
279 }
275 } 280 }