Mercurial > hg > monetdb-java
comparison tests/TLSTester.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 | 2fee4b71baac |
children | 157dcb2d6516 |
comparison
equal
deleted
inserted
replaced
813:a71afa48f269 | 834:5aa19bbed0d6 |
---|---|
9 import java.sql.DriverManager; | 9 import java.sql.DriverManager; |
10 import java.sql.SQLException; | 10 import java.sql.SQLException; |
11 import java.util.HashMap; | 11 import java.util.HashMap; |
12 import java.util.HashSet; | 12 import java.util.HashSet; |
13 import java.util.Properties; | 13 import java.util.Properties; |
14 import java.util.stream.Collectors; | |
15 | 14 |
16 public class TLSTester { | 15 public class TLSTester { |
17 int verbose = 0; | 16 final HashMap<String, File> fileCache = new HashMap<>(); |
18 String serverHost = null; | 17 int verbose = 0; |
19 String altHost = null; | 18 String serverHost = null; |
20 int serverPort = -1; | 19 String altHost = null; |
21 boolean enableTrusted = false; | 20 int serverPort = -1; |
22 File tempDir = null; | 21 boolean enableTrusted = false; |
23 final HashMap<String, File> fileCache = new HashMap<>(); | 22 File tempDir = null; |
24 private HashSet<String> preparedButNotRun = new HashSet<>(); | 23 private final HashSet<String> preparedButNotRun = new HashSet<>(); |
25 | 24 |
26 public TLSTester(String[] args) { | 25 public TLSTester(String[] args) { |
27 for (int i = 0; i < args.length; i++) { | 26 for (int i = 0; i < args.length; i++) { |
28 String arg = args[i]; | 27 String arg = args[i]; |
29 if (arg.equals("-v")) { | 28 if (arg.equals("-v")) { |
30 verbose = 1; | 29 verbose = 1; |
31 } else if (arg.equals("-a")) { | 30 } else if (arg.equals("-a")) { |
32 altHost = args[++i]; | 31 altHost = args[++i]; |
33 } else if (arg.equals("-t")) { | 32 } else if (arg.equals("-t")) { |
34 enableTrusted = true; | 33 enableTrusted = true; |
35 } else if (!arg.startsWith("-") && serverHost == null) { | 34 } else if (!arg.startsWith("-") && serverHost == null) { |
36 int idx = arg.indexOf(':'); | 35 int idx = arg.indexOf(':'); |
37 if (idx > 0) { | 36 if (idx > 0) { |
38 serverHost = arg.substring(0, idx); | 37 serverHost = arg.substring(0, idx); |
39 try { | 38 try { |
40 serverPort = Integer.parseInt(arg.substring(idx + 1)); | 39 serverPort = Integer.parseInt(arg.substring(idx + 1)); |
41 if (serverPort > 0 && serverPort < 65536) | 40 if (serverPort > 0 && serverPort < 65536) |
42 continue; | 41 continue; |
43 } catch (NumberFormatException ignored) { | 42 } catch (NumberFormatException ignored) { |
44 } | 43 } |
45 } | 44 } |
46 // if we get here it wasn't very valid | 45 // if we get here it wasn't very valid |
47 throw new IllegalArgumentException("Invalid argument: " + arg); | 46 throw new IllegalArgumentException("Invalid argument: " + arg); |
48 } else { | 47 } else { |
49 throw new IllegalArgumentException("Unexpected argument: " + arg); | 48 throw new IllegalArgumentException("Unexpected argument: " + arg); |
50 } | 49 } |
51 } | 50 } |
52 } | 51 } |
53 | 52 |
54 public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException { | 53 public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException { |
55 Class.forName("org.monetdb.jdbc.MonetDriver"); | 54 Class.forName("org.monetdb.jdbc.MonetDriver"); |
56 TLSTester main = new TLSTester(args); | 55 TLSTester main = new TLSTester(args); |
57 main.run(); | 56 main.run(); |
58 } | 57 } |
59 | 58 |
60 private HashMap<String,Integer> loadPortMap(String testName) throws IOException { | 59 private HashMap<String, Integer> loadPortMap(String testName) throws IOException { |
61 HashMap<String,Integer> portMap = new HashMap<>(); | 60 HashMap<String, Integer> portMap = new HashMap<>(); |
62 InputStream in = fetchData("/?test=" + testName); | 61 InputStream in = fetchData("/?test=" + testName); |
63 BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); | 62 BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); |
64 for (String line = br.readLine(); line != null; line = br.readLine()) { | 63 for (String line = br.readLine(); line != null; line = br.readLine()) { |
65 int idx = line.indexOf(':'); | 64 int idx = line.indexOf(':'); |
66 String service = line.substring(0, idx); | 65 String service = line.substring(0, idx); |
67 int port; | 66 int port; |
68 try { | 67 try { |
69 port = Integer.parseInt(line.substring(idx + 1)); | 68 port = Integer.parseInt(line.substring(idx + 1)); |
70 } catch (NumberFormatException e) { | 69 } catch (NumberFormatException e) { |
71 throw new RuntimeException("Invalid port map line: " + line); | 70 throw new RuntimeException("Invalid port map line: " + line); |
72 } | 71 } |
73 portMap.put(service, port); | 72 portMap.put(service, port); |
74 } | 73 } |
75 return portMap; | 74 return portMap; |
76 } | 75 } |
77 | 76 |
78 private File resource(String resource) throws IOException { | 77 private File resource(String resource) throws IOException { |
79 if (!fileCache.containsKey(resource)) | 78 if (!fileCache.containsKey(resource)) |
80 fetchResource(resource); | 79 fetchResource(resource); |
81 return fileCache.get(resource); | 80 return fileCache.get(resource); |
82 } | 81 } |
83 | 82 |
84 private void fetchResource(String resource) throws IOException { | 83 private void fetchResource(String resource) throws IOException { |
85 if (!resource.startsWith("/")) { | 84 if (!resource.startsWith("/")) { |
86 throw new IllegalArgumentException("Resource must start with slash: " + resource); | 85 throw new IllegalArgumentException("Resource must start with slash: " + resource); |
87 } | 86 } |
88 if (tempDir == null) { | 87 if (tempDir == null) { |
89 tempDir = Files.createTempDirectory("tlstest").toFile(); | 88 tempDir = Files.createTempDirectory("tlstest").toFile(); |
90 tempDir.deleteOnExit(); | 89 tempDir.deleteOnExit(); |
91 } | 90 } |
92 File outPath = new File(tempDir, resource.substring(1)); | 91 File outPath = new File(tempDir, resource.substring(1)); |
93 try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) { | 92 try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) { |
94 byte[] buffer = new byte[12]; | 93 byte[] buffer = new byte[12]; |
95 while (true) { | 94 while (true) { |
96 int n = in.read(buffer); | 95 int n = in.read(buffer); |
97 if (n <= 0) | 96 if (n <= 0) |
98 break; | 97 break; |
99 out.write(buffer, 0, n); | 98 out.write(buffer, 0, n); |
100 } | 99 } |
101 } | 100 } |
102 fileCache.put(resource, outPath); | 101 fileCache.put(resource, outPath); |
103 } | 102 } |
104 | 103 |
105 private byte[] fetchBytes(String resource) throws IOException { | 104 private byte[] fetchBytes(String resource) throws IOException { |
106 ByteArrayOutputStream out = new ByteArrayOutputStream(); | 105 ByteArrayOutputStream out = new ByteArrayOutputStream(); |
107 try (InputStream in = fetchData(resource)) { | 106 try (InputStream in = fetchData(resource)) { |
108 byte[] buffer = new byte[22]; | 107 byte[] buffer = new byte[22]; |
109 while (true) { | 108 while (true) { |
110 int nread = in.read(buffer); | 109 int nread = in.read(buffer); |
111 if (nread <= 0) | 110 if (nread <= 0) |
112 break; | 111 break; |
113 out.write(buffer, 0, nread); | 112 out.write(buffer, 0, nread); |
114 } | 113 } |
115 return out.toByteArray(); | 114 return out.toByteArray(); |
116 } | 115 } |
117 } | 116 } |
118 | 117 |
119 private InputStream fetchData(String resource) throws IOException { | 118 private InputStream fetchData(String resource) throws IOException { |
120 URL url = new URL("http://" + serverHost + ":" + serverPort + resource); | 119 URL url = new URL("http://" + serverHost + ":" + serverPort + resource); |
121 URLConnection conn = url.openConnection(); | 120 URLConnection conn = url.openConnection(); |
122 conn.connect(); | 121 conn.connect(); |
123 return conn.getInputStream(); | 122 return conn.getInputStream(); |
124 } | 123 } |
125 | 124 |
126 private void run() throws IOException, SQLException { | 125 private void run() throws IOException, SQLException { |
127 test_connect_plain(); | 126 test_connect_plain(); |
128 test_connect_tls(); | 127 test_connect_tls(); |
129 test_refuse_no_cert(); | 128 test_refuse_no_cert(); |
130 test_refuse_wrong_cert(); | 129 test_refuse_wrong_cert(); |
131 test_refuse_wrong_host(); | 130 test_refuse_wrong_host(); |
132 test_refuse_tlsv12(); | 131 test_refuse_tlsv12(); |
133 test_refuse_expired(); | 132 test_refuse_expired(); |
134 // test_connect_client_auth1(); | 133 // test_connect_client_auth1(); |
135 // test_connect_client_auth2(); | 134 // test_connect_client_auth2(); |
136 test_fail_tls_to_plain(); | 135 test_fail_tls_to_plain(); |
137 test_fail_plain_to_tls(); | 136 test_fail_plain_to_tls(); |
138 test_connect_server_name(); | 137 test_connect_server_name(); |
139 test_connect_alpn_mapi9(); | 138 test_connect_alpn_mapi9(); |
140 test_connect_trusted(); | 139 test_connect_trusted(); |
141 test_refuse_trusted_wrong_host(); | 140 test_refuse_trusted_wrong_host(); |
142 | 141 |
143 // did we forget to call expectSucceed and expectFailure somewhere? | 142 // did we forget to call expectSucceed and expectFailure somewhere? |
144 if (!preparedButNotRun.isEmpty()) { | 143 if (!preparedButNotRun.isEmpty()) { |
145 String names = String.join(", ", preparedButNotRun); | 144 String names = String.join(", ", preparedButNotRun); |
146 throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names); | 145 throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names); |
147 } | 146 } |
148 } | 147 } |
149 | 148 |
150 private void test_connect_plain() throws IOException, SQLException { | 149 private void test_connect_plain() throws IOException, SQLException { |
151 attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess(); | 150 attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess(); |
152 } | 151 } |
153 | 152 |
154 private void test_connect_tls() throws IOException, SQLException { | 153 private void test_connect_tls() throws IOException, SQLException { |
155 Attempt attempt = attempt("connect_tls", "server1"); | 154 Attempt attempt = attempt("connect_tls", "server1"); |
156 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); | 155 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); |
157 } | 156 } |
158 | 157 |
159 private void test_refuse_no_cert() throws IOException, SQLException { | 158 private void test_refuse_no_cert() throws IOException, SQLException { |
160 attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed"); | 159 attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed"); |
161 } | 160 } |
162 | 161 |
163 private void test_refuse_wrong_cert() throws IOException, SQLException { | 162 private void test_refuse_wrong_cert() throws IOException, SQLException { |
164 Attempt attempt = attempt("refuse_wrong_cert", "server1"); | 163 Attempt attempt = attempt("refuse_wrong_cert", "server1"); |
165 attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed"); | 164 attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed"); |
166 } | 165 } |
167 | 166 |
168 private void test_refuse_wrong_host() throws IOException, SQLException { | 167 private void test_refuse_wrong_host() throws IOException, SQLException { |
169 Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost); | 168 Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost); |
170 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name"); | 169 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name"); |
171 } | 170 } |
172 | 171 |
173 private void test_refuse_tlsv12() throws IOException, SQLException { | 172 private void test_refuse_tlsv12() throws IOException, SQLException { |
174 Attempt attempt = attempt("refuse_tlsv12", "tls12"); | 173 Attempt attempt = attempt("refuse_tlsv12", "tls12"); |
175 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version"); | 174 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version"); |
176 } | 175 } |
177 | 176 |
178 private void test_refuse_expired() throws IOException, SQLException { | 177 private void test_refuse_expired() throws IOException, SQLException { |
179 Attempt attempt = attempt("refuse_expired", "expiredcert"); | 178 Attempt attempt = attempt("refuse_expired", "expiredcert"); |
180 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed"); | 179 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed"); |
181 } | 180 } |
182 | 181 |
183 private void test_connect_client_auth1() throws IOException, SQLException { | 182 private void test_connect_client_auth1() throws IOException, SQLException { |
184 attempt("connect_client_auth1", "clientauth") | 183 attempt("connect_client_auth1", "clientauth") |
185 .withFile(Parameter.CERT, "/ca1.crt") | 184 .withFile(Parameter.CERT, "/ca1.crt") |
186 .withFile(Parameter.CLIENTKEY, "/client2.keycrt") | 185 .withFile(Parameter.CLIENTKEY, "/client2.keycrt") |
187 .expectSuccess(); | 186 .expectSuccess(); |
188 } | 187 } |
189 | 188 |
190 private void test_connect_client_auth2() throws IOException, SQLException { | 189 private void test_connect_client_auth2() throws IOException, SQLException { |
191 attempt("connect_client_auth2", "clientauth") | 190 attempt("connect_client_auth2", "clientauth") |
192 .withFile(Parameter.CERT, "/ca1.crt") | 191 .withFile(Parameter.CERT, "/ca1.crt") |
193 .withFile(Parameter.CLIENTKEY, "/client2.key") | 192 .withFile(Parameter.CLIENTKEY, "/client2.key") |
194 .withFile(Parameter.CLIENTCERT, "/client2.crt") | 193 .withFile(Parameter.CLIENTCERT, "/client2.crt") |
195 .expectSuccess(); | 194 .expectSuccess(); |
196 } | 195 } |
197 | 196 |
198 private void test_fail_tls_to_plain() throws IOException, SQLException { | 197 private void test_fail_tls_to_plain() throws IOException, SQLException { |
199 Attempt attempt = attempt("fail_tls_to_plain", "plain"); | 198 Attempt attempt = attempt("fail_tls_to_plain", "plain"); |
200 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure(""); | 199 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure(""); |
201 | 200 |
202 } | 201 } |
203 | 202 |
204 private void test_fail_plain_to_tls() throws IOException, SQLException { | 203 private void test_fail_plain_to_tls() throws IOException, SQLException { |
205 attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect"); | 204 attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect"); |
206 } | 205 } |
207 | 206 |
208 private void test_connect_server_name() throws IOException, SQLException { | 207 private void test_connect_server_name() throws IOException, SQLException { |
209 Attempt attempt = attempt("connect_server_name", "sni"); | 208 Attempt attempt = attempt("connect_server_name", "sni"); |
210 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); | 209 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); |
211 } | 210 } |
212 | 211 |
213 private void test_connect_alpn_mapi9() throws IOException, SQLException { | 212 private void test_connect_alpn_mapi9() throws IOException, SQLException { |
214 attempt("connect_alpn_mapi9", "alpn_mapi9") | 213 attempt("connect_alpn_mapi9", "alpn_mapi9").withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); |
215 .withFile(Parameter.CERT, "/ca1.crt") | 214 } |
216 .expectSuccess(); | 215 |
217 } | 216 private void test_connect_trusted() throws IOException, SQLException { |
218 | 217 attempt("connect_trusted", null) |
219 private void test_connect_trusted() throws IOException, SQLException { | 218 .with(Parameter.HOST, "monetdb.ergates.nl") |
220 attempt("connect_trusted", null) | 219 .with(Parameter.PORT, 50000) |
221 .with(Parameter.HOST, "monetdb.ergates.nl") | 220 .expectSuccess(); |
222 .with(Parameter.PORT, 50000) | 221 } |
223 .expectSuccess(); | 222 |
224 } | 223 private void test_refuse_trusted_wrong_host() throws IOException, SQLException { |
225 | 224 attempt("test_refuse_trusted_wrong_host", null) |
226 private void test_refuse_trusted_wrong_host() throws IOException, SQLException { | 225 .with(Parameter.HOST, "monetdbxyz.ergates.nl") |
227 attempt("test_refuse_trusted_wrong_host", null) | 226 .with(Parameter.PORT, 50000) |
228 .with(Parameter.HOST, "monetdbxyz.ergates.nl") | 227 .expectFailure("No subject alternative DNS name"); |
229 .with(Parameter.PORT, 50000) | 228 } |
230 .expectFailure("No subject alternative DNS name"); | 229 |
231 } | 230 private Attempt attempt(String testName, String portName) throws IOException { |
232 | 231 preparedButNotRun.add(testName); |
233 private Attempt attempt(String testName, String portName) throws IOException { | 232 return new Attempt(testName, portName); |
234 preparedButNotRun.add(testName); | 233 } |
235 return new Attempt(testName, portName); | 234 |
236 } | 235 private class Attempt { |
237 | 236 private final String testName; |
238 private class Attempt { | 237 private final Properties props = new Properties(); |
239 private final String testName; | 238 boolean disabled = false; |
240 private final Properties props = new Properties(); | 239 |
241 boolean disabled = false; | 240 public Attempt(String testName, String portName) throws IOException { |
242 | 241 HashMap<String, Integer> portMap = loadPortMap(testName); |
243 public Attempt(String testName, String portName) throws IOException { | 242 |
244 HashMap<String, Integer> portMap = loadPortMap(testName); | 243 this.testName = testName; |
245 | 244 with(Parameter.TLS, true); |
246 this.testName = testName; | 245 with(Parameter.HOST, serverHost); |
247 with(Parameter.TLS, true); | 246 with(Parameter.SO_TIMEOUT, 3000); |
248 with(Parameter.HOST, serverHost); | 247 if (portName != null) { |
249 with(Parameter.SO_TIMEOUT, 3000); | 248 Integer port = portMap.get(portName); |
250 if (portName != null) { | 249 if (port != null) { |
251 Integer port = portMap.get(portName); | 250 with(Parameter.PORT, port); |
252 if (port != null) { | 251 } else { |
253 with(Parameter.PORT, port); | 252 throw new RuntimeException("Unknown port name: " + portName); |
254 } else { | 253 } |
255 throw new RuntimeException("Unknown port name: " + portName); | 254 } |
256 } | 255 } |
257 } | 256 |
258 } | 257 private Attempt with(Parameter parm, String value) { |
259 | 258 props.setProperty(parm.name, value); |
260 private Attempt with(Parameter parm, String value) { | 259 return this; |
261 props.setProperty(parm.name, value); | 260 } |
262 return this; | 261 |
263 } | 262 private Attempt with(Parameter parm, int value) { |
264 | 263 props.setProperty(parm.name, Integer.toString(value)); |
265 private Attempt with(Parameter parm, int value) { | 264 return this; |
266 props.setProperty(parm.name, Integer.toString(value)); | 265 } |
267 return this; | 266 |
268 } | 267 private Attempt with(Parameter parm, boolean value) { |
269 | 268 props.setProperty(parm.name, value ? "true" : "false"); |
270 private Attempt with(Parameter parm, boolean value) { | 269 return this; |
271 props.setProperty(parm.name, value ? "true" : "false"); | 270 } |
272 return this; | 271 |
273 } | 272 private Attempt withFile(Parameter parm, String certResource) throws IOException { |
274 | 273 File certFile = resource(certResource); |
275 private Attempt withFile(Parameter parm, String certResource) throws IOException { | 274 String path = certFile.getPath(); |
276 File certFile = resource(certResource); | 275 with(parm, path); |
277 String path = certFile.getPath(); | 276 return this; |
278 with(parm, path); | 277 } |
279 return this; | 278 |
280 } | 279 public void expectSuccess() throws SQLException { |
281 | 280 preparedButNotRun.remove(testName); |
282 public void expectSuccess() throws SQLException { | 281 if (disabled) |
283 preparedButNotRun.remove(testName); | 282 return; |
284 if (disabled) | 283 try { |
285 return; | 284 Connection conn = DriverManager.getConnection("jdbc:monetdb:", props); |
286 try { | 285 conn.close(); |
287 Connection conn = DriverManager.getConnection("jdbc:monetdb:", props); | 286 } catch (SQLException e) { |
288 conn.close(); | 287 if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) { |
289 } catch (SQLException e) { | 288 // it looks like a failure but this is actually our success scenario |
290 if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) { | 289 // because this is what the TLS Tester does when the connection succeeds. |
291 // it looks like a failure but this is actually our success scenario | 290 return; |
292 // because this is what the TLS Tester does when the connection succeeds. | 291 } |
293 return; | 292 // other exceptions ARE errors and should be reported. |
294 } | 293 throw e; |
295 // other exceptions ARE errors and should be reported. | 294 } |
296 throw e; | 295 } |
297 } | 296 |
298 } | 297 public void expectFailure(String... expectedMessages) throws SQLException { |
299 | 298 if (disabled) |
300 public void expectFailure(String... expectedMessages) throws SQLException { | 299 return; |
301 if (disabled) | 300 try { |
302 return; | 301 expectSuccess(); |
303 try { | 302 throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't"); |
304 expectSuccess(); | 303 } catch (SQLException e) { |
305 throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't"); | 304 for (String expected : expectedMessages) |
306 } catch (SQLException e) { | 305 if (e.getMessage().contains(expected)) |
307 for (String expected : expectedMessages) | 306 return; |
308 if (e.getMessage().contains(expected)) | 307 String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n <" + String.join(">\n <", expectedMessages) + ">"; |
309 return; | 308 throw new RuntimeException(message); |
310 String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n <" + String.join(">\n <", expectedMessages) + ">"; | 309 |
311 throw new RuntimeException(message); | 310 } |
312 | 311 } |
313 } | 312 |
314 } | 313 } |
315 | |
316 } | |
317 } | 314 } |