Merge lp:~pbeaman/akiban-server/fix-direct-rest-bugs into lp:~akiban-technologies/akiban-server/trunk

Proposed by Peter Beaman
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
Reviewer Review Type Date Requested Status
Mike McMahon Approve
Review via email: mp+159960@code.launchpad.net

Description of the change

Fix several Akiban Direct issues itemized in bug1168547 and bug1168962.

Also, add new test framework, parallel to RestServiceFilesIT called RestServiceScriptIT. Difference is that RestServiceScriptIT reads test steps from a file with suffix ".script" and executes them in sequence. This allows tests to build over state over multiple REST calls. Several test scripts to verify fixes for the itemized bugs are included in src/test/resource/com/akiban/direct. Javadoc in the test framework describes how to write a script. Note: I did not use YAML for expediency - if there is strong sentiment to avoid defining yet another way of specifying tests, I would be willing to redo the script elements as YAML documents; however, this will cost some time and effort.

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.

To post a comment you must log in.
2637. By Peter Beaman

Add a LOG message when transaction retries more than 3 times

Revision history for this message
Mike McMahon (mmcm) wrote :

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.

RestServiceScriptsIT.java does not have a proper header.

review: Needs Fixing
2638. By Peter Beaman

Merge from trunk, fix HttpStatus import and bad copyright notice

Revision history for this message
Peter Beaman (pbeaman) wrote :

Thanks. Fixed.

Revision history for this message
Mike McMahon (mmcm) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+

Subscribers

People subscribed via source and target branches