Merge lp:~pbeaman/akiban-server/fix-direct-rest-bugs into lp:~akiban-technologies/akiban-server/trunk
- fix-direct-rest-bugs
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Mike McMahon |
Approved revision: | 2638 |
Merged at revision: | 2635 |
Proposed branch: | lp:~pbeaman/akiban-server/fix-direct-rest-bugs |
Merge into: | lp:~akiban-technologies/akiban-server/trunk |
Diff against target: |
989 lines (+680/-46) 12 files modified
src/main/java/com/akiban/direct/DirectException.java (+0/-2) src/main/java/com/akiban/rest/RestResponseBuilder.java (+3/-1) src/main/java/com/akiban/rest/resources/DefaultResource.java (+1/-1) src/main/java/com/akiban/server/service/restdml/DirectServiceImpl.java (+40/-32) src/main/java/com/akiban/server/service/restdml/EndpointMetadata.java (+28/-6) src/test/java/com/akiban/rest/RestServiceScriptsIT.java (+460/-0) src/test/resources/com/akiban/rest/direct/bad-direct-create.body (+26/-0) src/test/resources/com/akiban/rest/direct/direct-create.body (+4/-4) src/test/resources/com/akiban/rest/direct/missing-direct-create.body (+9/-0) src/test/resources/com/akiban/rest/direct/test-basic-direct-functions.script (+30/-0) src/test/resources/com/akiban/rest/direct/test-direct-registration-errors.script (+33/-0) src/test/resources/com/akiban/rest/direct/test-invalid-endpoint-errors.script (+46/-0) |
To merge this branch: | bzr merge lp:~pbeaman/akiban-server/fix-direct-rest-bugs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike McMahon | Approve | ||
Review via email: mp+159960@code.launchpad.net |
Commit message
Description of the change
Fix several Akiban Direct issues itemized in bug1168547 and bug1168962.
Also, add new test framework, parallel to RestServiceFilesIT called RestServiceScri
Specific changes:
- Loop for transaction retry limited to 3 iterations before an internal server error (500) is returned.
- Exception text returned to REST client now includes detailed cause information when _register method fails.
- When a Direct REST request fails to match a defined Direct end-point, the error now returned is NOT_FOUND (404).
- End-point matching logic now applies content type filter in all cases and avoid throwing an NPE when a content type is not specified.
- Registering a library that has no _register method no longer causes other valid libraries to be ignored.
- Validation now enforces that an end-point specification must be complete. For example, a missing IN definition is now caught during the attempt to register the end-point rather than causing an NPE when the end-pint is invoked.
- All valid Java names are now permitted in the end-point metadata specification for field requiring a name, such as the function name.
- 2637. By Peter Beaman
-
Add a LOG message when transaction retries more than 3 times
- 2638. By Peter Beaman
-
Merge from trunk, fix HttpStatus import and bad copyright notice
Peter Beaman (pbeaman) wrote : | # |
Thanks. Fixed.
Mike McMahon (mmcm) : | # |
Preview Diff
1 | === modified file 'src/main/java/com/akiban/direct/DirectException.java' |
2 | --- src/main/java/com/akiban/direct/DirectException.java 2013-03-22 20:05:57 +0000 |
3 | +++ src/main/java/com/akiban/direct/DirectException.java 2013-04-23 01:21:27 +0000 |
4 | @@ -26,8 +26,6 @@ |
5 | */ |
6 | public class DirectException extends RuntimeException { |
7 | |
8 | - private static final long serialVersionUID = 9217986178149864740L; |
9 | - |
10 | public DirectException(final Exception e) { |
11 | super(e); |
12 | } |
13 | |
14 | === modified file 'src/main/java/com/akiban/rest/RestResponseBuilder.java' |
15 | --- src/main/java/com/akiban/rest/RestResponseBuilder.java 2013-04-04 16:07:24 +0000 |
16 | +++ src/main/java/com/akiban/rest/RestResponseBuilder.java 2013-04-23 01:21:27 +0000 |
17 | @@ -145,7 +145,9 @@ |
18 | |
19 | public WebApplicationException wrapException(Throwable e) { |
20 | final ErrorCode code; |
21 | - if(e instanceof InvalidOperationException) { |
22 | + if (e instanceof WebApplicationException) { |
23 | + return (WebApplicationException)e; |
24 | + } else if(e instanceof InvalidOperationException) { |
25 | code = ((InvalidOperationException)e).getCode(); |
26 | } else if(e instanceof SQLException) { |
27 | code = ErrorCode.valueOfCode(((SQLException)e).getSQLState()); |
28 | |
29 | === modified file 'src/main/java/com/akiban/rest/resources/DefaultResource.java' |
30 | --- src/main/java/com/akiban/rest/resources/DefaultResource.java 2013-04-06 05:07:23 +0000 |
31 | +++ src/main/java/com/akiban/rest/resources/DefaultResource.java 2013-04-23 01:21:27 +0000 |
32 | @@ -67,7 +67,7 @@ |
33 | } |
34 | |
35 | |
36 | - private Response buildResponse(HttpServletRequest request) { |
37 | + static Response buildResponse(HttpServletRequest request) { |
38 | String msg = String.format("API %s not supported", request.getRequestURI()); |
39 | return RestResponseBuilder |
40 | .forRequest(request) |
41 | |
42 | === modified file 'src/main/java/com/akiban/server/service/restdml/DirectServiceImpl.java' |
43 | --- src/main/java/com/akiban/server/service/restdml/DirectServiceImpl.java 2013-04-16 00:56:31 +0000 |
44 | +++ src/main/java/com/akiban/server/service/restdml/DirectServiceImpl.java 2013-04-23 01:21:27 +0000 |
45 | @@ -33,10 +33,11 @@ |
46 | import java.util.regex.Matcher; |
47 | |
48 | import javax.servlet.http.HttpServletRequest; |
49 | +import javax.servlet.http.HttpServletResponse; |
50 | +import javax.ws.rs.WebApplicationException; |
51 | import javax.ws.rs.core.MediaType; |
52 | import javax.ws.rs.core.MultivaluedMap; |
53 | |
54 | -import com.fasterxml.jackson.core.JsonGenerator; |
55 | import org.slf4j.Logger; |
56 | import org.slf4j.LoggerFactory; |
57 | |
58 | @@ -52,7 +53,6 @@ |
59 | import com.akiban.rest.RestServiceImpl; |
60 | import com.akiban.rest.resources.ResourceHelper; |
61 | import com.akiban.server.error.ExternalRoutineInvocationException; |
62 | -import com.akiban.server.error.MalformedRequestException; |
63 | import com.akiban.server.error.NoSuchRoutineException; |
64 | import com.akiban.server.error.ScriptLibraryRegistrationException; |
65 | import com.akiban.server.service.Service; |
66 | @@ -70,6 +70,7 @@ |
67 | import com.akiban.server.types3.TInstance; |
68 | import com.akiban.sql.embedded.EmbeddedJDBCService; |
69 | import com.akiban.sql.embedded.JDBCConnection; |
70 | +import com.fasterxml.jackson.core.JsonGenerator; |
71 | import com.google.inject.Inject; |
72 | import com.persistit.exception.RollbackException; |
73 | |
74 | @@ -92,10 +93,7 @@ |
75 | private final static String IS_INOUT = "is_inout"; |
76 | private final static String IS_RESULT = "is_result"; |
77 | |
78 | - private final static String COMMENT_ANNOTATION1 = "//##"; |
79 | - private final static String COMMENT_ANNOTATION2 = "##//"; |
80 | - private final static String ENDPOINT = "endpoint"; |
81 | - |
82 | + private final static int TRANSACTION_RETRY_COUNT = 3; |
83 | private final static String DISTINGUISHED_REGISTRATION_METHOD_NAME = "_register"; |
84 | |
85 | private final static String CREATE_PROCEDURE_FORMAT = "CREATE OR REPLACE PROCEDURE \"%s\".\"%s\" ()" |
86 | @@ -205,8 +203,12 @@ |
87 | AkibanInformationSchema ais = dxlService.ddlFunctions().getAIS(session); |
88 | EndpointMap endpointMap = null; |
89 | |
90 | - if (functionsOnly) { |
91 | - endpointMap = getEndpointMap(session); |
92 | + try { |
93 | + if (functionsOnly) { |
94 | + endpointMap = getEndpointMap(session); |
95 | + } |
96 | + } catch (RegistrationException e) { |
97 | + throw new ScriptLibraryRegistrationException(e); |
98 | } |
99 | |
100 | if (module.isEmpty()) { |
101 | @@ -334,27 +336,30 @@ |
102 | final TableName procName, final String pathParams, final MultivaluedMap<String, String> queryParameters, |
103 | final byte[] content, final MediaType[] responseType) throws Exception { |
104 | try (JDBCConnection conn = jdbcConnection(request, procName.getSchemaName());) { |
105 | - |
106 | + LOG.debug("Invoking {} {}", request.getMethod(), request.getRequestURI()); |
107 | conn.setAutoCommit(false); |
108 | |
109 | boolean completed = false; |
110 | - boolean repeat = true; |
111 | + int repeat = TRANSACTION_RETRY_COUNT; |
112 | |
113 | - while (repeat) { |
114 | + while (--repeat >= 0) { |
115 | try { |
116 | Direct.enter(procName.getSchemaName(), dxlService.ddlFunctions().getAIS(conn.getSession())); |
117 | Direct.getContext().setConnection(conn); |
118 | - repeat = false; |
119 | conn.beginTransaction(); |
120 | invokeRestFunction(writer, conn, method, procName, pathParams, queryParameters, content, |
121 | request.getContentType(), responseType); |
122 | conn.commitTransaction(); |
123 | completed = true; |
124 | + return; |
125 | } catch (RollbackException e) { |
126 | - repeat = true; |
127 | - } catch (Exception e) { |
128 | - e.printStackTrace(); |
129 | - throw e; |
130 | + if (repeat == 0) { |
131 | + LOG.error("Transaction failed " + TRANSACTION_RETRY_COUNT + " times: " |
132 | + + request.getRequestURI()); |
133 | + throw new WebApplicationException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
134 | + } |
135 | + } catch (RegistrationException e) { |
136 | + throw new ScriptLibraryRegistrationException(e); |
137 | } finally { |
138 | try { |
139 | if (!completed) { |
140 | @@ -389,24 +394,22 @@ |
141 | |
142 | EndpointMetadata md = selectEndpoint(list, pathParams, requestType, responseType, cache); |
143 | if (md == null) { |
144 | - // TODO - Is this the correct Exception? Is there a way to convey |
145 | - // this without logged stack trace? |
146 | - throw new MalformedRequestException("No matching endpoint"); |
147 | + throw new WebApplicationException(HttpServletResponse.SC_NOT_FOUND); |
148 | } |
149 | |
150 | final Object[] args = createArgsArray(pathParams, queryParameters, content, cache, md); |
151 | |
152 | - final ScriptPool<ScriptLibrary> libraryPool = conn.getRoutineLoader() |
153 | - .getScriptLibrary(conn.getSession(), new TableName(procName.getSchemaName(), |
154 | - md.routineName)); |
155 | + final ScriptPool<ScriptLibrary> libraryPool = conn.getRoutineLoader().getScriptLibrary(conn.getSession(), |
156 | + new TableName(procName.getSchemaName(), md.routineName)); |
157 | final ScriptLibrary library = libraryPool.get(); |
158 | boolean success = false; |
159 | Object result; |
160 | + |
161 | + LOG.debug("Endpoint {}", md); |
162 | try { |
163 | result = library.invoke(md.function, args); |
164 | success = true; |
165 | - } |
166 | - finally { |
167 | + } finally { |
168 | libraryPool.put(library, success); |
169 | } |
170 | |
171 | @@ -468,13 +471,13 @@ |
172 | EndpointMetadata md = null; |
173 | if (list != null) { |
174 | for (final EndpointMetadata candidate : list) { |
175 | + if (requestType != null && candidate.expectedContentType != null |
176 | + && !requestType.startsWith(candidate.expectedContentType)) { |
177 | + continue; |
178 | + } |
179 | if (candidate.pattern != null) { |
180 | Matcher matcher = candidate.getParamPathMatcher(cache, pathParams); |
181 | if (matcher.matches()) { |
182 | - if (responseType != null && candidate.expectedContentType != null |
183 | - && !requestType.startsWith(candidate.expectedContentType)) { |
184 | - continue; |
185 | - } |
186 | md = candidate; |
187 | break; |
188 | } |
189 | @@ -541,7 +544,8 @@ |
190 | for (final Routine routine : ais.getRoutines().values()) { |
191 | if (routine.getCallingConvention().equals(CallingConvention.SCRIPT_LIBRARY) |
192 | && routine.getDynamicResultSets() == 0 && routine.getParameters().isEmpty()) { |
193 | - final ScriptPool<ScriptLibrary> libraryPool = routineLoader.getScriptLibrary(session, routine.getName()); |
194 | + final ScriptPool<ScriptLibrary> libraryPool = routineLoader.getScriptLibrary(session, |
195 | + routine.getName()); |
196 | final ScriptLibrary library = libraryPool.get(); |
197 | boolean success = false; |
198 | try { |
199 | @@ -549,6 +553,8 @@ |
200 | new Object[] { new RestFunctionRegistrar() { |
201 | @Override |
202 | public void register(String specification) throws Exception { |
203 | + LOG.debug("Registering endpoint in routine {}: {}", routine.getName(), |
204 | + specification); |
205 | EndpointMap.this.register(routine.getName().getSchemaName(), routine.getName() |
206 | .getTableName(), specification); |
207 | } |
208 | @@ -558,7 +564,7 @@ |
209 | if (e.getCause() instanceof NoSuchMethodException) { |
210 | LOG.warn("Script library " + routine.getName() + " has no _register function"); |
211 | success = true; |
212 | - return; |
213 | + continue; |
214 | } |
215 | Throwable previous = e; |
216 | Throwable current; |
217 | @@ -570,8 +576,10 @@ |
218 | } |
219 | throw e; |
220 | } catch (RegistrationException e) { |
221 | + LOG.warn("Endpoint registration failure {} in routine {}", e, routine.getName()); |
222 | throw e; |
223 | } catch (Exception e) { |
224 | + LOG.warn("Endpoint registration failure {} in routine {}", e, routine.getName()); |
225 | throw new RegistrationException(e); |
226 | } finally { |
227 | libraryPool.put(library, success); |
228 | @@ -580,7 +588,6 @@ |
229 | } |
230 | } |
231 | |
232 | - |
233 | void register(final String schema, final String routine, final String spec) throws Exception { |
234 | |
235 | try { |
236 | @@ -596,7 +603,8 @@ |
237 | list.add(em); |
238 | } |
239 | } catch (Exception e) { |
240 | - throw new RegistrationException("Invalid function specification: " + spec, e); |
241 | + String msg = e instanceof IllegalArgumentException ? e.getMessage() : ""; |
242 | + throw new RegistrationException("Invalid function specification: " + spec + " - " + msg, e); |
243 | } |
244 | } |
245 | } |
246 | |
247 | === modified file 'src/main/java/com/akiban/server/service/restdml/EndpointMetadata.java' |
248 | --- src/main/java/com/akiban/server/service/restdml/EndpointMetadata.java 2013-04-16 00:56:31 +0000 |
249 | +++ src/main/java/com/akiban/server/service/restdml/EndpointMetadata.java 2013-04-23 01:21:27 +0000 |
250 | @@ -267,6 +267,7 @@ |
251 | + specification); |
252 | } |
253 | } |
254 | + em.validate(); |
255 | return em; |
256 | } |
257 | |
258 | @@ -314,7 +315,6 @@ |
259 | } |
260 | } |
261 | return pm; |
262 | - |
263 | } |
264 | |
265 | /** |
266 | @@ -506,6 +506,8 @@ |
267 | String s; |
268 | if (content instanceof byte[]) { |
269 | s = new String((byte[]) content, UTF8); |
270 | + } else if (content == null) { |
271 | + s = ""; |
272 | } else { |
273 | s = (String) content; |
274 | } |
275 | @@ -660,7 +662,7 @@ |
276 | index++; |
277 | break; |
278 | } |
279 | - if (!Character.isLetterOrDigit(c) || (first && !Character.isLetter(c))) { |
280 | + if (!Character.isJavaIdentifierPart(c) || (first && !Character.isJavaIdentifierStart(c))) { |
281 | throw new IllegalArgumentException("Invalid character in name: " + source); |
282 | } |
283 | result.append(c); |
284 | @@ -751,6 +753,22 @@ |
285 | } |
286 | } |
287 | |
288 | + private void validate() { |
289 | + notNull(schemaName, "schema name"); |
290 | + notNull(routineName, "routine name"); |
291 | + notNull(method, "'method='"); |
292 | + notNull(name, "'path='"); |
293 | + notNull(function, "'function='"); |
294 | + notNull(inParams, "'in='"); |
295 | + notNull(outParam, "'out='"); |
296 | + } |
297 | + |
298 | + private void notNull(final Object v, final String name) { |
299 | + if (v == null) { |
300 | + throw new IllegalArgumentException(name + " not specified in " + toString()); |
301 | + } |
302 | + } |
303 | + |
304 | @Override |
305 | public String toString() { |
306 | StringBuilder sb = new StringBuilder(); |
307 | @@ -759,11 +777,15 @@ |
308 | append(sb, pattern.toString()); |
309 | } |
310 | append(sb, " ", FUNCTION, "=", function, " ", IN, "=("); |
311 | - for (int index = 0; index < inParams.length; index++) { |
312 | - if (index > 0) { |
313 | - sb.append(", "); |
314 | + if (inParams == null) { |
315 | + sb.append("null"); |
316 | + } else { |
317 | + for (int index = 0; index < inParams.length; index++) { |
318 | + if (index > 0) { |
319 | + sb.append(", "); |
320 | + } |
321 | + sb.append(inParams[index]); |
322 | } |
323 | - sb.append(inParams[index]); |
324 | } |
325 | append(sb, ") ", OUT, "="); |
326 | if (outParam == null) { |
327 | |
328 | === added file 'src/test/java/com/akiban/rest/RestServiceScriptsIT.java' |
329 | --- src/test/java/com/akiban/rest/RestServiceScriptsIT.java 1970-01-01 00:00:00 +0000 |
330 | +++ src/test/java/com/akiban/rest/RestServiceScriptsIT.java 2013-04-23 01:21:27 +0000 |
331 | @@ -0,0 +1,460 @@ |
332 | +/** |
333 | + * Copyright (C) 2009-2013 Akiban Technologies, Inc. |
334 | + * |
335 | + * This program is free software: you can redistribute it and/or modify |
336 | + * it under the terms of the GNU Affero General Public License as published by |
337 | + * the Free Software Foundation, either version 3 of the License, or |
338 | + * (at your option) any later version. |
339 | + * |
340 | + * This program is distributed in the hope that it will be useful, |
341 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
342 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
343 | + * GNU Affero General Public License for more details. |
344 | + * |
345 | + * You should have received a copy of the GNU Affero General Public License |
346 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
347 | + */ |
348 | + |
349 | +package com.akiban.rest; |
350 | + |
351 | +import static com.akiban.util.JsonUtils.readTree; |
352 | +import static org.junit.Assert.fail; |
353 | + |
354 | +import java.io.ByteArrayInputStream; |
355 | +import java.io.File; |
356 | +import java.io.IOException; |
357 | +import java.io.UnsupportedEncodingException; |
358 | +import java.net.MalformedURLException; |
359 | +import java.net.URISyntaxException; |
360 | +import java.net.URL; |
361 | +import java.net.URLEncoder; |
362 | +import java.util.ArrayList; |
363 | +import java.util.Arrays; |
364 | +import java.util.Collection; |
365 | +import java.util.Comparator; |
366 | +import java.util.List; |
367 | +import java.util.Map; |
368 | + |
369 | +import junit.framework.ComparisonFailure; |
370 | + |
371 | +import org.eclipse.jetty.client.ContentExchange; |
372 | +import org.eclipse.jetty.client.HttpClient; |
373 | +import org.eclipse.jetty.client.HttpExchange; |
374 | +import org.junit.After; |
375 | +import org.junit.Test; |
376 | +import org.junit.runner.RunWith; |
377 | +import org.slf4j.Logger; |
378 | +import org.slf4j.LoggerFactory; |
379 | + |
380 | +import com.akiban.http.HttpConductor; |
381 | +import com.akiban.junit.NamedParameterizedRunner; |
382 | +import com.akiban.junit.Parameterization; |
383 | +import com.akiban.server.service.is.BasicInfoSchemaTablesService; |
384 | +import com.akiban.server.service.is.BasicInfoSchemaTablesServiceImpl; |
385 | +import com.akiban.server.service.servicemanager.GuicedServiceManager; |
386 | +import com.akiban.server.test.it.ITBase; |
387 | +import com.akiban.sql.RegexFilenameFilter; |
388 | +import com.akiban.util.Strings; |
389 | +import com.fasterxml.jackson.core.JsonParseException; |
390 | +import com.fasterxml.jackson.databind.JsonNode; |
391 | + |
392 | +/** |
393 | + * Scripted tests for REST end-points. Code was largely copied from |
394 | + * RestServiceFilesIT. Difference is that this version finds files with the |
395 | + * suffix ".script" and executes the command stream located in them. Commands are: |
396 | + * |
397 | + * <pre> |
398 | + * GET address |
399 | + * DELETE address |
400 | + * QUERY query |
401 | + * EXPLAIN query |
402 | + * POST address content |
403 | + * PUT address content |
404 | + * PATCH address content |
405 | + * EQUALS expected |
406 | + * CONTAINS expected |
407 | + * JSONEQ expected |
408 | + * HEADERS expected |
409 | + * EMPTY |
410 | + * NOTEMPTY |
411 | + * </pre> |
412 | + * |
413 | + * where address is a path relative the resource end-point, content is a string |
414 | + * value that is converted to bytes and sent with POST, PUT and PATCH |
415 | + * operations, and expected is a value used in comparison with the most recently |
416 | + * returned content. The values of the query, content and expected fields may be |
417 | + * specified in-line, or as a reference to another file as in @filename. For |
418 | + * in-line values, the character sequences "\n", "\t" and "\r" are converted to |
419 | + * the corresponding new-line, tab and return characters. This transformation is |
420 | + * not done if the value is supplied as a file reference. An empty string can be |
421 | + * specified as simply @, e.g.: |
422 | + * |
423 | + * <pre> |
424 | + * POST /builder/implode/test.customers @ |
425 | + * </pre> |
426 | + * |
427 | + * @author peter |
428 | + */ |
429 | +@RunWith(NamedParameterizedRunner.class) |
430 | +public class RestServiceScriptsIT extends ITBase { |
431 | + private static final Logger LOG = LoggerFactory.getLogger(RestServiceScriptsIT.class.getName()); |
432 | + |
433 | + private static final File RESOURCE_DIR = new File("src/test/resources/" |
434 | + + RestServiceScriptsIT.class.getPackage().getName().replace('.', '/')); |
435 | + |
436 | + public static final String SCHEMA_NAME = "test"; |
437 | + |
438 | + private static class CaseParams { |
439 | + public final String subDir; |
440 | + public final String caseName; |
441 | + public final String script; |
442 | + |
443 | + private CaseParams(String subDir, String caseName, String script) { |
444 | + this.subDir = subDir; |
445 | + this.caseName = caseName; |
446 | + this.script = script; |
447 | + } |
448 | + } |
449 | + |
450 | + static class Result { |
451 | + HttpExchange conn; |
452 | + String output = "<not executed>"; |
453 | + } |
454 | + |
455 | + protected final CaseParams caseParams; |
456 | + protected final HttpClient httpClient; |
457 | + private final List<String> errors = new ArrayList<>(); |
458 | + private final Result result = new Result(); |
459 | + private int lineNumber = 0; |
460 | + |
461 | + public RestServiceScriptsIT(CaseParams caseParams) throws Exception { |
462 | + this.caseParams = caseParams; |
463 | + this.httpClient = new HttpClient(); |
464 | + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); |
465 | + httpClient.setMaxConnectionsPerAddress(10); |
466 | + httpClient.start(); |
467 | + } |
468 | + |
469 | + @Override |
470 | + protected GuicedServiceManager.BindingsConfigurationProvider serviceBindingsProvider() { |
471 | + return super.serviceBindingsProvider().bindAndRequire(RestService.class, RestServiceImpl.class) |
472 | + .bindAndRequire(BasicInfoSchemaTablesService.class, BasicInfoSchemaTablesServiceImpl.class); |
473 | + } |
474 | + |
475 | + @Override |
476 | + protected Map<String, String> startupConfigProperties() { |
477 | + return uniqueStartupConfigProperties(RestServiceScriptsIT.class); |
478 | + } |
479 | + |
480 | + public static File[] gatherRequestFiles(File dir) { |
481 | + File[] result = dir.listFiles(new RegexFilenameFilter(".*\\.(script)")); |
482 | + Arrays.sort(result, new Comparator<File>() { |
483 | + public int compare(File f1, File f2) { |
484 | + return f1.getName().compareTo(f2.getName()); |
485 | + } |
486 | + }); |
487 | + return result; |
488 | + } |
489 | + |
490 | + @NamedParameterizedRunner.TestParameters |
491 | + public static Collection<Parameterization> gatherCases() throws Exception { |
492 | + Collection<Parameterization> result = new ArrayList<>(); |
493 | + for (String subDirName : RESOURCE_DIR.list()) { |
494 | + File subDir = new File(RESOURCE_DIR, subDirName); |
495 | + if (!subDir.isDirectory()) { |
496 | + LOG.warn("Skipping unexpected file: {}", subDir); |
497 | + continue; |
498 | + } |
499 | + for (File requestFile : gatherRequestFiles(subDir)) { |
500 | + String inputName = requestFile.getName(); |
501 | + int dotIndex = inputName.lastIndexOf('.'); |
502 | + String caseName = inputName.substring(0, dotIndex); |
503 | + String script = Strings.dumpFileToString(requestFile); |
504 | + |
505 | + result.add(Parameterization.create(subDirName + File.separator + caseName, new CaseParams(subDirName, |
506 | + caseName, script))); |
507 | + } |
508 | + } |
509 | + return result; |
510 | + } |
511 | + |
512 | + private URL getRestURL(String request) throws MalformedURLException { |
513 | + int port = serviceManager().getServiceByClass(HttpConductor.class).getPort(); |
514 | + String context = serviceManager().getServiceByClass(RestService.class).getContextPath(); |
515 | + return new URL("http", "localhost", port, context + request); |
516 | + } |
517 | + |
518 | + private void loadDatabase(String subDirName) throws Exception { |
519 | + File subDir = new File(RESOURCE_DIR, subDirName); |
520 | + File schemaFile = new File(subDir, "schema.ddl"); |
521 | + if (schemaFile.exists()) { |
522 | + loadSchemaFile(SCHEMA_NAME, schemaFile); |
523 | + } |
524 | + for (File data : subDir.listFiles(new RegexFilenameFilter(".*\\.dat"))) { |
525 | + loadDataFile(SCHEMA_NAME, data); |
526 | + } |
527 | + } |
528 | + |
529 | + private static void postContents(HttpExchange httpConn, byte[] request) throws IOException { |
530 | + httpConn.setRequestContentType("application/json"); |
531 | + httpConn.setRequestHeader("Accept", "application/json"); |
532 | + httpConn.setRequestContentSource(new ByteArrayInputStream(request)); |
533 | + } |
534 | + |
535 | + @After |
536 | + public void finish() throws Exception { |
537 | + httpClient.stop(); |
538 | + } |
539 | + |
540 | + private void error(String message) { |
541 | + error(message, result.output); |
542 | + } |
543 | + |
544 | + private void error(String message, String s) { |
545 | + String error = String.format("%s in %s:%d <%s>", message, caseParams.caseName, lineNumber, s); |
546 | + errors.add(error); |
547 | + } |
548 | + |
549 | + @Test |
550 | + public void testRequest() throws Exception { |
551 | + loadDatabase(caseParams.subDir); |
552 | + |
553 | + // Execute lines of script |
554 | + |
555 | + result.conn = null; |
556 | + result.output = "<not executed>"; |
557 | + lineNumber = 0; |
558 | + |
559 | + try { |
560 | + for (String line : caseParams.script.split("\n")) { |
561 | + lineNumber++; |
562 | + line = line.trim(); |
563 | + |
564 | + while (line.contains(" ")) { |
565 | + line = line.replace(" ", " "); |
566 | + } |
567 | + if (line.startsWith("#") || line.isEmpty()) { |
568 | + continue; |
569 | + } |
570 | + String[] pieces = line.split(" "); |
571 | + String command = pieces[0].toUpperCase(); |
572 | + |
573 | + switch (command) { |
574 | + case "DEBUG": |
575 | + System.out.println("DEBUG executed on line " + lineNumber); |
576 | + break; |
577 | + case "GET": |
578 | + case "DELETE": { |
579 | + result.conn = null; |
580 | + if (pieces.length < 2) { |
581 | + error("Missing argument"); |
582 | + continue; |
583 | + } |
584 | + executeRestCall(command, pieces[1], null); |
585 | + break; |
586 | + } |
587 | + case "QUERY": |
588 | + result.conn = null; |
589 | + if (pieces.length < 2) { |
590 | + error("Missing argument"); |
591 | + continue; |
592 | + } |
593 | + executeRestCall("GET", "/sql/query?q=" + trimAndURLEncode(value(line, 1)), null); |
594 | + break; |
595 | + case "EXPLAIN": |
596 | + result.conn = null; |
597 | + if (pieces.length < 2) { |
598 | + error("Missing argument"); |
599 | + continue; |
600 | + } |
601 | + executeRestCall("GET", "/sql/explain?q=" + trimAndURLEncode(value(line, 1)), null); |
602 | + break; |
603 | + case "POST": |
604 | + case "PUT": |
605 | + case "PATCH": { |
606 | + result.conn = null; |
607 | + pieces = line.split(" ", 3); |
608 | + if (pieces.length < 3) { |
609 | + error("Missing argument"); |
610 | + continue; |
611 | + } |
612 | + String contents = value(line, 2); |
613 | + executeRestCall(command, pieces[1], contents); |
614 | + break; |
615 | + } |
616 | + case "EQUALS": |
617 | + if (pieces.length < 2) { |
618 | + error("Missing argument"); |
619 | + continue; |
620 | + } |
621 | + compareStrings("Incorrect response", value(line, 1), result.output); |
622 | + break; |
623 | + case "CONTAINS": |
624 | + if (pieces.length < 2) { |
625 | + error("Missing argument"); |
626 | + continue; |
627 | + } |
628 | + if (!result.output.contains(value(line, 1))) { |
629 | + error("Incorrect response"); |
630 | + } |
631 | + break; |
632 | + case "JSONEQ": |
633 | + if (pieces.length < 2) { |
634 | + error("Missing argument"); |
635 | + continue; |
636 | + } |
637 | + compareAsJSON("Unexpected response", value(line, 1), result.output); |
638 | + break; |
639 | + case "HEADERS": |
640 | + if (pieces.length < 2) { |
641 | + error("Missing argument"); |
642 | + continue; |
643 | + } |
644 | + compareHeaders(result.conn, value(line, 1)); |
645 | + break; |
646 | + case "NOTEMPTY": |
647 | + if (result.output.isEmpty() || result.conn == null) { |
648 | + error("Expected non-empty response"); |
649 | + continue; |
650 | + } |
651 | + break; |
652 | + case "EMPTY": |
653 | + if (!result.output.isEmpty()) { |
654 | + error("Expected empty response"); |
655 | + } |
656 | + break; |
657 | + default: |
658 | + result.conn = null; |
659 | + error("Unknown script command '" + command + "'"); |
660 | + } |
661 | + } |
662 | + } finally { |
663 | + result.conn = null; |
664 | + } |
665 | + if (!errors.isEmpty()) { |
666 | + String failMessage = "Failed with " + errors.size() + " errors:"; |
667 | + for (String s : errors) { |
668 | + failMessage += "\n " + s; |
669 | + } |
670 | + fail(failMessage); |
671 | + } |
672 | + } |
673 | + |
674 | + private void executeRestCall(final String command, final String address, final String contents) throws Exception { |
675 | + String[] pieces = address.split("\\|"); |
676 | + try { |
677 | + result.conn = openConnection(pieces[0], command); |
678 | + if (contents != null) { |
679 | + postContents(result.conn, contents.getBytes()); |
680 | + } |
681 | + // After postContents to override default |
682 | + if (pieces.length > 1) { |
683 | + result.conn.setRequestContentType(pieces[1]); |
684 | + } |
685 | + httpClient.send(result.conn); |
686 | + result.conn.waitForDone(); |
687 | + result.output = getOutput(result.conn); |
688 | + } catch (Exception e) { |
689 | + result.output = e.toString(); |
690 | + } finally { |
691 | + if (result.conn != null) { |
692 | + fullyDisconnect(result.conn); |
693 | + } |
694 | + } |
695 | + } |
696 | + |
697 | + private HttpExchange openConnection(String address, String requestMethod) throws IOException, URISyntaxException { |
698 | + URL url = getRestURL(address); |
699 | + HttpExchange exchange = new ContentExchange(true); |
700 | + exchange.setURI(url.toURI()); |
701 | + exchange.setMethod(requestMethod); |
702 | + return exchange; |
703 | + } |
704 | + |
705 | + private String getOutput(HttpExchange httpConn) throws IOException { |
706 | + return ((ContentExchange) httpConn).getResponseContent(); |
707 | + } |
708 | + |
709 | + private String value(String line, int index) throws IOException { |
710 | + String s = line.split(" ", index + 1)[index]; |
711 | + if (s.startsWith("@")) { |
712 | + if (s.length() == 1) { |
713 | + s = ""; |
714 | + } else { |
715 | + s = Strings.dumpFileToString(new File(new File(RESOURCE_DIR, caseParams.subDir), s.substring(1))); |
716 | + } |
717 | + } else { |
718 | + s = s.replace("\\n", "\n").replace("\\n", "\t"); |
719 | + } |
720 | + return s; |
721 | + } |
722 | + |
723 | + private static String trimAndURLEncode(String s) throws UnsupportedEncodingException { |
724 | + return URLEncoder.encode(s.trim().replaceAll("\\s+", " "), "UTF-8"); |
725 | + } |
726 | + |
727 | + private String diff(String a, String b) { |
728 | + return new ComparisonFailure("", a, b).getMessage(); |
729 | + } |
730 | + |
731 | + private void compareStrings(String assertMsg, String expected, String actual) { |
732 | + if (!expected.equals(actual)) { |
733 | + error(assertMsg, diff(expected, actual)); |
734 | + } |
735 | + } |
736 | + |
737 | + private void compareAsJSON(String assertMsg, String expected, String actual) throws IOException { |
738 | + JsonNode expectedNode = null; |
739 | + JsonNode actualNode = null; |
740 | + String expectedTrimmed = (expected != null) ? expected.trim() : ""; |
741 | + String actualTrimmed = (actual != null) ? actual.trim() : ""; |
742 | + try { |
743 | + if (!expectedTrimmed.isEmpty()) { |
744 | + expectedNode = readTree(expected); |
745 | + } |
746 | + if (!actualTrimmed.isEmpty()) { |
747 | + actualNode = readTree(actual); |
748 | + } |
749 | + } catch (JsonParseException e) { |
750 | + // Note: This case handles the jsonp tests. Somewhat fragile, but |
751 | + // not horrible yet. |
752 | + } |
753 | + // Try manual equals and then assert strings for pretty print |
754 | + if (expectedNode != null && actualNode != null) { |
755 | + if (!expectedNode.equals(actualNode)) { |
756 | + error(assertMsg, diff(expectedNode.toString(), actualNode.toString())); |
757 | + } |
758 | + } else { |
759 | + compareStrings(assertMsg, expected, actual); |
760 | + } |
761 | + } |
762 | + |
763 | + private void compareHeaders(HttpExchange httpConn, String checkHeaders) throws Exception { |
764 | + ContentExchange exch = (ContentExchange) httpConn; |
765 | + |
766 | + String[] headerList = checkHeaders.split(Strings.NL); |
767 | + for (String header : headerList) { |
768 | + String[] nameValue = header.split(":", 2); |
769 | + |
770 | + if (nameValue[0].equals("responseCode")) { |
771 | + if (Integer.parseInt(nameValue[1].trim()) != exch.getResponseStatus()) { |
772 | + error("Incorrect Response Status", |
773 | + String.format("%d expected %s", exch.getResponseStatus(), nameValue[1])); |
774 | + } |
775 | + } else { |
776 | + if (!nameValue[1].trim().equals(exch.getResponseFields().getStringField(nameValue[0]))) { |
777 | + error("Incorrect Response Header", String.format("%s expected %s", exch.getResponseFields() |
778 | + .getStringField(nameValue[0]), nameValue[1].trim())); |
779 | + } |
780 | + } |
781 | + } |
782 | + } |
783 | + |
784 | + private void fullyDisconnect(HttpExchange httpConn) throws InterruptedException { |
785 | + // If there is a failure, leaving junk in any of the streams can cause |
786 | + // cascading issues. |
787 | + // Get rid of anything left and disconnect. |
788 | + httpConn.waitForDone(); |
789 | + httpConn.reset(); |
790 | + } |
791 | +} |
792 | |
793 | === added file 'src/test/resources/com/akiban/rest/direct/bad-direct-create.body' |
794 | --- src/test/resources/com/akiban/rest/direct/bad-direct-create.body 1970-01-01 00:00:00 +0000 |
795 | +++ src/test/resources/com/akiban/rest/direct/bad-direct-create.body 2013-04-23 01:21:27 +0000 |
796 | @@ -0,0 +1,26 @@ |
797 | +function _register(registrar) { |
798 | + registrar.register( |
799 | + "method=GET path=cnames function=customerNames out=String"); |
800 | + |
801 | + registrar.register( |
802 | + "method=GET path=oids/(\\d*) function=orderNumbers in=(pp:1 int required)"); |
803 | +}; |
804 | + |
805 | +function customerNames(s) { |
806 | + var result = s; |
807 | + var extent = Packages.com.akiban.direct.Direct.context.extent; |
808 | + for (customer in Iterator(extent.customers)) { |
809 | + result += "," + customer.name; |
810 | + } |
811 | + return result; |
812 | +} |
813 | + |
814 | +function orderNumbers(cid) { |
815 | + var result = s; |
816 | + var extent = Packages.com.akiban.direct.Direct.context.extent; |
817 | + var customer = extent.getCustomer(cid); |
818 | + for (order in customer.orders) { |
819 | + result += "," + order.oid; |
820 | + } |
821 | + return result; |
822 | +} |
823 | |
824 | === modified file 'src/test/resources/com/akiban/rest/direct/direct-create.body' |
825 | --- src/test/resources/com/akiban/rest/direct/direct-create.body 2013-04-08 17:42:31 +0000 |
826 | +++ src/test/resources/com/akiban/rest/direct/direct-create.body 2013-04-23 01:21:27 +0000 |
827 | @@ -1,12 +1,12 @@ |
828 | function _register(registrar) { |
829 | registrar.register( |
830 | - "method=GET path=cnames function=customerNames in=(QP:prefix String required) out=String"); |
831 | + "method=GET path=cnames function=customer_Names in=(QP:prefix String required) out=String"); |
832 | |
833 | registrar.register( |
834 | - "method=GET path=oids/(\\d*) function=orderNumbers in=(pp:1 int required) out=String"); |
835 | + "method=GET path=oids/(\\d*) function=order_Numbers in=(pp:1 int required) out=String"); |
836 | }; |
837 | |
838 | -function customerNames(s) { |
839 | +function customer_Names(s) { |
840 | var result = s; |
841 | var extent = Packages.com.akiban.direct.Direct.context.extent; |
842 | for (customer in Iterator(extent.customers)) { |
843 | @@ -15,7 +15,7 @@ |
844 | return result; |
845 | } |
846 | |
847 | -function orderNumbers(cid) { |
848 | +function order_Numbers(cid) { |
849 | var result = s; |
850 | var extent = Packages.com.akiban.direct.Direct.context.extent; |
851 | var customer = extent.getCustomer(cid); |
852 | |
853 | === added file 'src/test/resources/com/akiban/rest/direct/missing-direct-create.body' |
854 | --- src/test/resources/com/akiban/rest/direct/missing-direct-create.body 1970-01-01 00:00:00 +0000 |
855 | +++ src/test/resources/com/akiban/rest/direct/missing-direct-create.body 2013-04-23 01:21:27 +0000 |
856 | @@ -0,0 +1,9 @@ |
857 | + |
858 | +function customerNames(s) { |
859 | + var result = s; |
860 | + var extent = Packages.com.akiban.direct.Direct.context.extent; |
861 | + for (customer in Iterator(extent.customers)) { |
862 | + result += "," + customer.name; |
863 | + } |
864 | + return result; |
865 | +} |
866 | |
867 | === added file 'src/test/resources/com/akiban/rest/direct/test-basic-direct-functions.script' |
868 | --- src/test/resources/com/akiban/rest/direct/test-basic-direct-functions.script 1970-01-01 00:00:00 +0000 |
869 | +++ src/test/resources/com/akiban/rest/direct/test-basic-direct-functions.script 2013-04-23 01:21:27 +0000 |
870 | @@ -0,0 +1,30 @@ |
871 | +# Install a library with two endpoints |
872 | +# |
873 | +PUT /direct/library?module=test.proc1&language=Javascript @direct-create.body |
874 | +EQUALS {"functions":2}\n |
875 | + |
876 | +# Delete the library |
877 | +# |
878 | +DELETE /direct/library?module=test.proc1&language=Javascript |
879 | +EQUALS {"functions":0}\n |
880 | + |
881 | +# Reinstall it |
882 | +# |
883 | +PUT /direct/library?module=test.proc1&language=Javascript @direct-create.body |
884 | + |
885 | +# Verify that it runs and consumes the supplied parameter value correctly |
886 | +# |
887 | +GET /direct/call/test.cnames?prefix=SomeParameterValue |
888 | +EQUALS SomeParameterValue,John Smith,Willy Jones,Jane Smith,Jonathan Smyth\n |
889 | +GET /direct/call/test.cnames?prefix=SomeOtherParameterValue |
890 | +EQUALS SomeOtherParameterValue,John Smith,Willy Jones,Jane Smith,Jonathan Smyth\n |
891 | + |
892 | +# Delete the library# |
893 | +# |
894 | +DELETE /direct/library?module=test.proc1&language=Javascript |
895 | + |
896 | +# Attempt to invoke end point should return a 404 |
897 | +# |
898 | +GET /direct/call/test.cnames?prefix=SomeOtherParameterValue |
899 | +CONTAINS Not Found |
900 | +CONTAINS 404 |
901 | |
902 | === added file 'src/test/resources/com/akiban/rest/direct/test-direct-registration-errors.script' |
903 | --- src/test/resources/com/akiban/rest/direct/test-direct-registration-errors.script 1970-01-01 00:00:00 +0000 |
904 | +++ src/test/resources/com/akiban/rest/direct/test-direct-registration-errors.script 2013-04-23 01:21:27 +0000 |
905 | @@ -0,0 +1,33 @@ |
906 | +# Normal registration |
907 | +# |
908 | +PUT /direct/library?module=test.proc1&language=Javascript @direct-create.body |
909 | +EQUALS {"functions":2}\n |
910 | + |
911 | +# Attempt to register an invalid specification |
912 | +# |
913 | +PUT /direct/library?module=test.proc2&language=Javascript @bad-direct-create.body |
914 | +CONTAINS Invalid function specification |
915 | + |
916 | +# Register a script with no _register function |
917 | +# |
918 | +PUT /direct/library?module=test.proc2&language=Javascript @missing-direct-create.body |
919 | +EQUALS {"functions":2}\n |
920 | + |
921 | +# Verify a valid script is still available |
922 | +# |
923 | +GET /direct/call/test.cnames?prefix=SomeParameterValue |
924 | +EQUALS SomeParameterValue,John Smith,Willy Jones,Jane Smith,Jonathan Smyth\n |
925 | + |
926 | +# Try adding broken scripts in a different order |
927 | +# |
928 | +DELETE /direct/library?module=test.proc1 |
929 | +DELETE /direct/library?module=test.proc2 |
930 | +PUT /direct/library?module=test.proc1&language=Javascript @missing-direct-create.body |
931 | +PUT /direct/library?module=test.proc2&language=Javascript @bad-direct-create.body |
932 | +PUT /direct/library?module=test.proc3&language=Javascript @direct-create.body |
933 | + |
934 | +# Verify a valid script is still available |
935 | +# |
936 | +GET /direct/call/test.cnames?prefix=SomeParameterValue |
937 | +EQUALS SomeParameterValue,John Smith,Willy Jones,Jane Smith,Jonathan Smyth\n |
938 | + |
939 | |
940 | === added file 'src/test/resources/com/akiban/rest/direct/test-invalid-endpoint-errors.script' |
941 | --- src/test/resources/com/akiban/rest/direct/test-invalid-endpoint-errors.script 1970-01-01 00:00:00 +0000 |
942 | +++ src/test/resources/com/akiban/rest/direct/test-invalid-endpoint-errors.script 2013-04-23 01:21:27 +0000 |
943 | @@ -0,0 +1,46 @@ |
944 | +# Normal registration |
945 | +# |
946 | +PUT /direct/library?module=test.proc1&language=Javascript @direct-create.body |
947 | +EQUALS {"functions":2}\n |
948 | + |
949 | +# Try mismatched methods - expect 404 errors |
950 | +# |
951 | +PUT /direct/call/test.cnames?prefix=SomeParameterValue @ |
952 | +CONTAINS 404 |
953 | +CONTAINS Not Found |
954 | +POST /direct/call/test.cnames?prefix=SomeParameterValue @ |
955 | +CONTAINS 404 |
956 | +CONTAINS Not Found |
957 | +DELETE /direct/call/test.cnames?prefix=SomeParameterValue @ |
958 | +CONTAINS 404 |
959 | +CONTAINS Not Found |
960 | + |
961 | +PUT /direct/library?module=test.proc1&language=Javascript function _register(registrar) {registrar.register("method=GET path=x function=x in=(JSON:prefix String required) out=String");}; |
962 | +GET /direct/call/test.x|text/plain |
963 | +CONTAINS 404 |
964 | +CONTAINS Not Found |
965 | + |
966 | +# Mismatched method GET / POST |
967 | +# |
968 | +PUT /direct/library?module=test.proc1&language=Javascript function _register(registrar) {registrar.register("method=GET path=x function=x in=(JSON:prefix String required) out=String");}; |
969 | +POST /direct/call/test.x|text/plain {"prefix": "SomeParamaterValue"} |
970 | +CONTAINS 404 |
971 | +CONTAINS Not Found |
972 | + |
973 | +# Mismatched method POST / PUT |
974 | +# |
975 | +PUT /direct/library?module=test.proc1&language=Javascript function _register(registrar) {registrar.register("method=POST path=x function=x in=(JSON:prefix String required) out=String");}; |
976 | +PUT /direct/call/test.x|application/json {"prefix": "SomeParamaterValue"} |
977 | +CONTAINS 404 |
978 | +CONTAINS Not Found |
979 | + |
980 | +# Mismatched request type application/json / text/plain |
981 | +# |
982 | +PUT /direct/library?module=test.proc1&language=Javascript function _register(registrar) {registrar.register("method=POST path=x function=x in=(JSON:prefix String required) out=String");}; |
983 | +POST /direct/call/test.x|text/plain {"prefix": "SomeParamaterValue"} |
984 | +CONTAINS 404 |
985 | +CONTAINS Not Found |
986 | + |
987 | + |
988 | + |
989 | + |
Looks okay but doesn't compile.
There is no org.apache. http.HttpStatus . httpcomponents core isn't in the pom. I think HttpServletResponse will give the necessary SC_ constants, if that's all that's needed.
RestServiceScri ptsIT.java does not have a proper header.