/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.rest;
import com.foundationdb.junit.SelectedParameterizedRunner;
import com.foundationdb.server.service.text.FullTextIndexService;
import com.foundationdb.http.HttpConductor;
import com.foundationdb.server.service.is.BasicInfoSchemaTablesService;
import com.foundationdb.server.service.servicemanager.GuicedServiceManager;
import com.foundationdb.server.service.text.FullTextIndexServiceImpl;
import com.foundationdb.server.test.it.ITBase;
import com.foundationdb.sql.RegexFilenameFilter;
import com.foundationdb.util.JsonUtils;
import com.foundationdb.util.Strings;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import static com.foundationdb.util.JsonUtils.readTree;
import static org.junit.Assert.assertEquals;
@RunWith(SelectedParameterizedRunner.class)
public class RestServiceFilesIT extends ITBase {
private static final Logger LOG = LoggerFactory.getLogger(RestServiceFilesIT.class.getName());
private static final File RESOURCE_DIR = new File(
"src/test/resources/" + RestServiceFilesIT.class.getPackage().getName().replace('.', '/')
);
public static final String SCHEMA_NAME = "test";
private static class CaseParams {
public final String subDir;
public final String caseName;
public final String requestMethod;
public final String requestURI;
public final String requestBody;
public final String expectedHeader;
public final String expectedResponse;
public final boolean expectedIgnore;
public final String checkURI;
public final String checkExpected;
public final String properties;
private CaseParams(String subDir, String caseName,
String requestMethod, String requestURI, String requestBody,
String expectedHeader, String expectedResponse, boolean expectedIgnore,
String checkURI, String checkExpected, String properties) {
this.subDir = subDir;
this.caseName = caseName;
this.requestMethod = requestMethod;
this.requestURI = requestURI;
this.requestBody = requestBody;
this.expectedHeader = expectedHeader;
this.expectedResponse = expectedResponse;
this.expectedIgnore = expectedIgnore;
this.checkURI = checkURI;
this.checkExpected = checkExpected;
this.properties = properties;
}
}
protected final CaseParams caseParams;
protected final HttpClient httpClient;
public RestServiceFilesIT(String name, CaseParams caseParams) throws Exception {
this.caseParams = caseParams;
this.httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.setMaxConnectionsPerAddress(10);
httpClient.start();
}
@Override
protected GuicedServiceManager.BindingsConfigurationProvider serviceBindingsProvider() {
return super.serviceBindingsProvider()
.require(RestService.class)
.bindAndRequire(FullTextIndexService.class, FullTextIndexServiceImpl.class)
.require(BasicInfoSchemaTablesService.class);
}
@Override
protected Map<String,String> startupConfigProperties() {
Map<String,String> config = new HashMap<>(super.startupConfigProperties());
config.put("fdbsql.rest.resource", "entity,fulltext,procedurecall,sql,security,version,direct,view");
config.put("fdbsql.http.csrf_protection.type", "none");
if ( caseParams.properties != null) {
for (String line : caseParams.properties.split("\\r?\\n")) {
String[] property = line.split("\\t");
if( property.length == 2) {
config.put(property[0], property[1]);
}
}
}
return config;
}
public static File[] gatherRequestFiles(File dir) {
File[] result = dir.listFiles(new RegexFilenameFilter(".*\\.(get|put|post|delete|query|explain|patch)"));
Arrays.sort(result, new Comparator<File>() {
public int compare(File f1, File f2) {
return f1.getName().compareTo(f2.getName());
}
});
return result;
}
private static String dumpFileIfExists(File file) throws IOException {
if(file.exists()) {
return Strings.dumpFileToString(file);
}
return null;
}
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> gatherCases() throws Exception {
Collection<Object[]> result = new ArrayList<>();
for(String subDirName: RESOURCE_DIR.list()) {
File subDir = new File(RESOURCE_DIR, subDirName);
if(!subDir.isDirectory()) {
LOG.warn("Skipping unexpected file: {}", subDir);
continue;
}
for(File requestFile : gatherRequestFiles(subDir)) {
String inputName = requestFile.getName();
int dotIndex = inputName.lastIndexOf('.');
String caseName = inputName.substring(0, dotIndex);
String basePath = requestFile.getParent() + File.separator + caseName;
String method = inputName.substring(dotIndex + 1).toUpperCase();
String uri = Strings.dumpFileToString(requestFile).trim();
String body = dumpFileIfExists(new File(basePath + ".body"));
String header = dumpFileIfExists(new File(basePath + ".expected_header"));
String expected = dumpFileIfExists(new File(basePath + ".expected"));
boolean expectedIgnore = new File(basePath + ".expected_ignore").exists();
String checkURI = dumpFileIfExists(new File(basePath + ".check"));
String checkExpected = dumpFileIfExists(new File(basePath + ".check_expected"));
String setParameters = dumpFileIfExists(new File(basePath + ".properties"));
if("QUERY".equals(method) || "EXPLAIN".equals(method)) {
body = buildJsonBody("q", uri);
uri = "QUERY".equals(method) ? "/sql/query" : "/sql/explain";
method = "POST";
}
result.add(new Object[]{
subDirName + File.separator + caseName,
new CaseParams(subDirName, caseName, method, uri, body,
header, expected, expectedIgnore,
checkURI, checkExpected, setParameters)
});
}
}
return result;
}
private static String buildJsonBody(String key, String value) throws IOException {
StringWriter stringWriter = new StringWriter();
JsonGenerator jsonGenerator = JsonUtils.createJsonGenerator(stringWriter);
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(key, value);
jsonGenerator.writeEndObject();
jsonGenerator.flush();
jsonGenerator.close();
return stringWriter.toString();
}
private URL getRestURL(String request) throws MalformedURLException {
int port = serviceManager().getServiceByClass(HttpConductor.class).getPort();
String context = serviceManager().getServiceByClass(RestService.class).getContextPath();
return new URL("http", "localhost", port, context + request);
}
private void loadDatabase(String subDirName, FullTextIndexService ftService) throws Exception {
File subDir = new File(RESOURCE_DIR, subDirName);
File schemaFile = new File(subDir, "schema.ddl");
if(schemaFile.exists()) {
loadSchemaFile(SCHEMA_NAME, schemaFile);
}
for (File data : subDir.listFiles(new RegexFilenameFilter(".*\\.dat"))) {
loadDataFile(SCHEMA_NAME, data);
}
String waitNeeded = dumpFileIfExists(new File(subDir, "full_text_background_wait"));
if(waitNeeded != null) {
ftService.backgroundWait();
}
}
public void checkRequest() throws Exception {
if (caseParams.checkURI != null && caseParams.checkExpected != null) {
HttpExchange httpConn = openConnection(getRestURL(caseParams.checkURI.trim()), "GET");
httpClient.send(httpConn);
httpConn.waitForDone();
try {
String actual = getOutput (httpConn);
compareExpected (caseParams.caseName + " check expected response ", caseParams.checkExpected, actual);
} finally {
fullyDisconnect(httpConn);
}
}
}
private static void postContents(HttpExchange httpConn, byte[] request) throws IOException {
httpConn.setRequestContentType("application/json");
httpConn.setRequestHeader("Accept", "application/json");
httpConn.setRequestContentSource(new ByteArrayInputStream(request));
}
@After
public void finish() throws Exception {
httpClient.stop();
}
@Test
public void testRequest() throws Exception {
loadDatabase(caseParams.subDir, serviceManager().getServiceByClass(FullTextIndexService.class));
HttpExchange conn = openConnection(getRestURL(caseParams.requestURI), caseParams.requestMethod);
try {
// Request
if (caseParams.requestMethod.equals("POST") ||
caseParams.requestMethod.equals("PUT") ||
caseParams.requestMethod.equals("PATCH")) {
if (caseParams.requestBody == null) {
throw new UnsupportedOperationException ("PUT/POST/PATCH expects request body (<test>.body)");
}
LOG.debug(caseParams.requestBody);
postContents(conn, caseParams.requestBody.getBytes());
} // else GET || DELETE
httpClient.send(conn);
conn.waitForDone();
// Response
String actual = getOutput(conn);
if(!caseParams.expectedIgnore) {
compareExpected(caseParams.requestMethod + " response", caseParams.expectedResponse, actual);
}
if (caseParams.expectedHeader != null) {
compareHeaders(conn, caseParams.expectedHeader);
}
} finally {
fullyDisconnect(conn);
}
checkRequest();
}
private HttpExchange openConnection(URL url, String requestMethod) throws IOException, URISyntaxException {
HttpExchange exchange = new ContentExchange(caseParams.expectedHeader != null);
exchange.setURI(url.toURI());
exchange.setMethod(requestMethod);
return exchange;
}
private String getOutput(HttpExchange httpConn) throws IOException {
return ((ContentExchange)httpConn).getResponseContent();
}
private void compareExpected(String assertMsg, String expected, String actual) throws IOException {
JsonNode expectedNode = null;
JsonNode actualNode = null;
boolean skipNodeCheck = false;
String expectedTrimmed = (expected != null) ? expected.trim() : "";
String actualTrimmed = (actual != null) ? actual.trim() : "";
try {
if(!expectedTrimmed.isEmpty()) {
expectedNode = readTree(expected);
}
if(!actualTrimmed.isEmpty()) {
actualNode = readTree(actual);
}
} catch(JsonParseException e) {
// Note: This case handles the jsonp tests. Somewhat fragile, but not horrible yet.
assertEquals(assertMsg, expectedTrimmed.replace("\r", "").replace("\n", ""),
actualTrimmed.replace("\r", "").replace("\n", ""));
skipNodeCheck = true;
}
// Try manual equals and then assert strings for pretty print
if(expectedNode != null && actualNode != null && !expectedNode.equals(actualNode)) {
assertEquals(assertMsg, expectedNode.toString(), actualNode.toString());
} else if(!skipNodeCheck) {
assertEquals(assertMsg, expectedNode, actualNode);
}
}
private void compareHeaders (HttpExchange httpConn, String checkHeaders) throws Exception {
ContentExchange exch = (ContentExchange)httpConn;
String[] headerList = checkHeaders.split("\n");
for (String header : headerList) {
String[] nameValue = header.split(":", 2);
if (nameValue[0].equals("responseCode")) {
assertEquals ("Headers Response", Integer.parseInt(nameValue[1].trim()),
exch.getResponseStatus());
} else {
assertEquals ("Headers check", nameValue[1].trim(),
exch.getResponseFields().getStringField(nameValue[0]));
}
}
}
public boolean isJson(String string) {
try {
JsonUtils.readTree(string);
return true;
} catch (IOException e) {
return false;
}
}
private void fullyDisconnect(HttpExchange httpConn) throws InterruptedException {
// If there is a failure, leaving junk in any of the streams can cause cascading issues.
// Get rid of anything left and disconnect.
httpConn.waitForDone();
httpConn.reset();
}
}