You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metamodel.apache.org by ka...@apache.org on 2017/09/29 06:35:10 UTC

metamodel-membrane git commit: Improved postman tests and logging for what's going on

Repository: metamodel-membrane
Updated Branches:
  refs/heads/postman-updates [created] b4abdb01a


Improved postman tests and logging for what's going on

Project: http://git-wip-us.apache.org/repos/asf/metamodel-membrane/repo
Commit: http://git-wip-us.apache.org/repos/asf/metamodel-membrane/commit/b4abdb01
Tree: http://git-wip-us.apache.org/repos/asf/metamodel-membrane/tree/b4abdb01
Diff: http://git-wip-us.apache.org/repos/asf/metamodel-membrane/diff/b4abdb01

Branch: refs/heads/postman-updates
Commit: b4abdb01af450d5c05fe650aed282eb150cf921f
Parents: 516a053
Author: Kasper Sørensen <i....@gmail.com>
Authored: Thu Sep 28 23:27:31 2017 -0700
Committer: Kasper Sørensen <i....@gmail.com>
Committed: Thu Sep 28 23:28:00 2017 -0700

----------------------------------------------------------------------
 Dockerfile                                      |   2 +-
 .../controllers/DataSourceController.java       |   4 +-
 .../membrane/controllers/QueryController.java   |  15 +-
 .../membrane/controllers/RestErrorHandler.java  |  35 +-
 .../controllers/TableDataController.java        |  14 +-
 .../membrane/controllers/TenantController.java  |   7 +
 postman-tests/Membrane.postman_collection.json  | 391 ++++++++++++++++++-
 .../metamodel/membrane/server/WebServer.java    |  12 +
 8 files changed, 438 insertions(+), 42 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/Dockerfile
----------------------------------------------------------------------
diff --git a/Dockerfile b/Dockerfile
index a10b5d8..a7598d4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-FROM maven:3.5-jdk-8-alpine
+FROM maven:3.5-jdk-9-slim
 
 # Set data directory used for the app's persistence
 VOLUME /data

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java
index 1efcfed..0dea52e 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java
@@ -90,7 +90,7 @@ public class DataSourceController {
 
         final String dataSourceIdentifier = dataSourceRegistry.registerDataSource(dataSourceId, properties);
 
-        logger.info("Created data source: {}/{}", tenantId, dataSourceIdentifier);
+        logger.info("{}/{} - Created data source", tenantId, dataSourceIdentifier);
 
         return get(tenantId, dataSourceIdentifier);
     }
@@ -139,7 +139,7 @@ public class DataSourceController {
         final DataSourceRegistry dataSourceRegistry = tenantContext.getDataSourceRegistry();
         dataSourceRegistry.removeDataSource(dataSourceName);
 
-        logger.info("Deleted data source: {}/{}", tenantId, dataSourceName);
+        logger.info("{}/{} - Deleted data source", tenantId, dataSourceName);
 
         return new DeleteDatasourceResponse().deleted(true).type("datasource").name(dataSourceName);
     }

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/core/src/main/java/org/apache/metamodel/membrane/controllers/QueryController.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/QueryController.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/QueryController.java
index 1fefa27..33a0567 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/QueryController.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/QueryController.java
@@ -29,6 +29,8 @@ import org.apache.metamodel.membrane.app.TenantContext;
 import org.apache.metamodel.membrane.app.TenantRegistry;
 import org.apache.metamodel.membrane.swagger.model.QueryResponse;
 import org.apache.metamodel.query.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -39,10 +41,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
-@RequestMapping(value = { "/{tenant}/{dataContext}/query",
-        "/{tenant}/{dataContext}/q" }, produces = MediaType.APPLICATION_JSON_VALUE)
+@RequestMapping(value = { "/{tenant}/{dataContext}/query", "/{tenant}/{dataContext}/q" },
+        produces = MediaType.APPLICATION_JSON_VALUE)
 public class QueryController {
 
+    private static final Logger logger = LoggerFactory.getLogger(QueryController.class);
+
     private final TenantRegistry tenantRegistry;
 
     @Autowired
@@ -62,10 +66,11 @@ public class QueryController {
 
         final Query query = dataContext.parseQuery(queryString);
 
-        return executeQuery(dataContext, query, offset, limit);
+        return executeQuery(tenantContext, dataSourceName, dataContext, query, offset, limit);
     }
 
-    public static QueryResponse executeQuery(DataContext dataContext, Query query, Integer offset, Integer limit) {
+    public static QueryResponse executeQuery(TenantContext tenant, String dataSource, DataContext dataContext,
+            Query query, Integer offset, Integer limit) {
 
         if (offset != null) {
             query.setFirstRow(offset);
@@ -74,6 +79,8 @@ public class QueryController {
             query.setMaxRows(limit);
         }
 
+        logger.info("{}/{} - Executing query: {}", tenant.getTenantName(), dataSource, query);
+
         final List<String> headers;
         final List<List<Object>> data = new ArrayList<>();
 

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/core/src/main/java/org/apache/metamodel/membrane/controllers/RestErrorHandler.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/RestErrorHandler.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/RestErrorHandler.java
index 1b5ec4d..4feab87 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/RestErrorHandler.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/RestErrorHandler.java
@@ -23,6 +23,8 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.apache.metamodel.membrane.app.exceptions.AbstractIdentifierNamingException;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceAlreadyExistException;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceNotUpdateableException;
@@ -35,6 +37,8 @@ import org.apache.metamodel.membrane.app.exceptions.NoSuchTenantException;
 import org.apache.metamodel.membrane.app.exceptions.TenantAlreadyExistException;
 import org.apache.metamodel.membrane.controllers.model.RestErrorResponse;
 import org.apache.metamodel.query.parser.QueryParserException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
@@ -48,9 +52,10 @@ import org.springframework.web.bind.annotation.ResponseStatus;
 @ControllerAdvice
 public class RestErrorHandler {
 
+    private static final Logger logger = LoggerFactory.getLogger(RestErrorHandler.class);
+
     /**
-     * Method binding issues (raised by Spring framework) - mapped to
-     * BAD_REQUEST.
+     * Method binding issues (raised by Spring framework) - mapped to BAD_REQUEST.
      * 
      * @param ex
      * @return
@@ -70,8 +75,8 @@ public class RestErrorHandler {
         final List<FieldError> fieldErrors = result.getFieldErrors();
         final Map<String, Object> fieldErrorsMap = new LinkedHashMap<>();
         for (FieldError fieldError : fieldErrors) {
-            fieldErrorsMap.put(fieldError.getObjectName() + '.' + fieldError.getField(), fieldError
-                    .getDefaultMessage());
+            fieldErrorsMap.put(fieldError.getObjectName() + '.' + fieldError.getField(),
+                    fieldError.getDefaultMessage());
         }
 
         final Map<String, Object> additionalDetails = new LinkedHashMap<>();
@@ -81,8 +86,8 @@ public class RestErrorHandler {
         if (!fieldErrorsMap.isEmpty()) {
             additionalDetails.put("field-errors", fieldErrorsMap);
         }
-        final RestErrorResponse errorResponse = new RestErrorResponse(HttpStatus.BAD_REQUEST.value(),
-                "Failed to validate request");
+        final RestErrorResponse errorResponse =
+                new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "Failed to validate request");
         if (!additionalDetails.isEmpty()) {
             errorResponse.setAdditionalDetails(additionalDetails);
         }
@@ -117,8 +122,7 @@ public class RestErrorHandler {
     }
 
     /**
-     * DataSource not updateable exception handler method - mapped to
-     * BAD_REQUEST.
+     * DataSource not updateable exception handler method - mapped to BAD_REQUEST.
      * 
      * @param ex
      * @return
@@ -127,13 +131,12 @@ public class RestErrorHandler {
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ResponseBody
     public RestErrorResponse processDataSourceNotUpdateable(DataSourceNotUpdateableException ex) {
-        return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "DataSource not updateable: " + ex
-                .getDataSourceName());
+        return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(),
+                "DataSource not updateable: " + ex.getDataSourceName());
     }
-    
+
     /**
-     * DataSource invalid exception handler method - mapped to
-     * BAD_REQUEST.
+     * DataSource invalid exception handler method - mapped to BAD_REQUEST.
      * 
      * @param ex
      * @return
@@ -142,8 +145,7 @@ public class RestErrorHandler {
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ResponseBody
     public RestErrorResponse processDataSourceNotUpdateable(InvalidDataSourceException ex) {
-        return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "DataSource invalid: " + ex
-                .getMessage());
+        return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "DataSource invalid: " + ex.getMessage());
     }
 
     /**
@@ -168,7 +170,8 @@ public class RestErrorHandler {
     @ExceptionHandler(Exception.class)
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ResponseBody
-    public RestErrorResponse processAnyException(Exception ex) {
+    public RestErrorResponse processAnyException(HttpServletRequest req, Exception ex) {
+        logger.error("{} {} - Unexpected error!", req.getMethod(), req.getRequestURI(), ex);
         final Map<String, Object> additionalDetails = new HashMap<>();
         additionalDetails.put("exception_type", ex.getClass().getName());
         return new RestErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), additionalDetails);

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/core/src/main/java/org/apache/metamodel/membrane/controllers/TableDataController.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/TableDataController.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/TableDataController.java
index afbc37c..f31f690 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/TableDataController.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/TableDataController.java
@@ -19,6 +19,7 @@
 package org.apache.metamodel.membrane.controllers;
 
 import java.math.BigDecimal;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -50,6 +51,8 @@ import org.apache.metamodel.query.SelectItem;
 import org.apache.metamodel.schema.Column;
 import org.apache.metamodel.schema.Table;
 import org.apache.metamodel.update.RowUpdationBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -68,6 +71,7 @@ import com.google.common.collect.Lists;
         "/{tenant}/{dataContext}/s/{schema}/t/{table}/d" }, produces = MediaType.APPLICATION_JSON_VALUE)
 public class TableDataController {
 
+    private static final Logger logger = LoggerFactory.getLogger(TableDataController.class);
     private final TenantRegistry tenantRegistry;
 
     @Autowired
@@ -90,7 +94,7 @@ public class TableDataController {
 
         final Query query = dataContext.query().from(table).selectAll().toQuery();
 
-        return QueryController.executeQuery(dataContext, query, offset, limit);
+        return QueryController.executeQuery(tenantContext, dataSourceName, dataContext, query, offset, limit);
     }
 
     @RequestMapping(method = RequestMethod.POST)
@@ -107,6 +111,10 @@ public class TableDataController {
 
         final Table table = traverser.getTable(schemaId, tableId);
 
+        logger.info("{}/{}/s/{}/t/{} - Data update: {} updates, {} deletes, {} inserts", tenantContext.getTenantName(),
+                dataSourceName, schemaId, tableId, size(postDataReq.getUpdate()), size(postDataReq.getDelete()),
+                size(postDataReq.getInsert()));
+
         final UpdateSummary result = dataContext.executeUpdate(new UpdateScript() {
             @Override
             public void run(UpdateCallback callback) {
@@ -163,6 +171,10 @@ public class TableDataController {
         return response;
     }
 
+    private static int size(Collection<?> col) {
+        return col == null ? 0 : col.size();
+    }
+
     private void setWhere(WhereClauseBuilder<?> whereBuilder, Table table, List<WhereCondition> conditions) {
         for (WhereCondition condition : conditions) {
             final Column column = table.getColumnByName(condition.getColumn());

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/core/src/main/java/org/apache/metamodel/membrane/controllers/TenantController.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/TenantController.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/TenantController.java
index 76197a1..e94983b 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/TenantController.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/TenantController.java
@@ -29,6 +29,8 @@ import org.apache.metamodel.membrane.app.TenantRegistry;
 import org.apache.metamodel.membrane.swagger.model.DeleteTenantResponse;
 import org.apache.metamodel.membrane.swagger.model.GetTenantResponse;
 import org.apache.metamodel.membrane.swagger.model.GetTenantResponseDatasources;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -41,6 +43,7 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping(value = "/{tenant}", produces = MediaType.APPLICATION_JSON_VALUE)
 public class TenantController {
 
+    private static final Logger logger = LoggerFactory.getLogger(TenantController.class);
     private final TenantRegistry tenantRegistry;
 
     @Autowired
@@ -75,6 +78,8 @@ public class TenantController {
         final TenantContext tenantContext = tenantRegistry.createTenantContext(tenantName);
         final String tenantIdentifier = tenantContext.getTenantName();
 
+        logger.info("{} - Created tenant", tenantIdentifier);
+
         final GetTenantResponse resp = new GetTenantResponse();
         resp.type("tenant");
         resp.name(tenantIdentifier);
@@ -87,6 +92,8 @@ public class TenantController {
     public DeleteTenantResponse deleteTenant(@PathVariable("tenant") String tenantName) {
         tenantRegistry.deleteTenantContext(tenantName);
 
+        logger.info("{} - Deleted tenant", tenantName);
+
         final DeleteTenantResponse resp = new DeleteTenantResponse();
         resp.type("tenant");
         resp.name(tenantName);

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/postman-tests/Membrane.postman_collection.json
----------------------------------------------------------------------
diff --git a/postman-tests/Membrane.postman_collection.json b/postman-tests/Membrane.postman_collection.json
index d8f0896..62e816f 100644
--- a/postman-tests/Membrane.postman_collection.json
+++ b/postman-tests/Membrane.postman_collection.json
@@ -2,12 +2,37 @@
 	"variables": [],
 	"info": {
 		"name": "Membrane",
-		"_postman_id": "084c91ce-fccc-8fc4-a3c4-1dc5d5b41388",
+		"_postman_id": "1265d16a-b1fd-26a4-7ea5-d2e4015df97e",
 		"description": "",
 		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
 	},
 	"item": [
 		{
+			"name": "Get swagger file",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"title is Apache MetaModel Membrane\"] = jsonData.info.title === \"Apache MetaModel Membrane\";"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/swagger.json",
+				"method": "GET",
+				"header": [],
+				"body": {},
+				"description": ""
+			},
+			"response": []
+		},
+		{
 			"name": "Create my-tenant",
 			"event": [
 				{
@@ -18,7 +43,10 @@
 							"tests[\"Status code is 200\"] = responseCode.code === 200;",
 							"",
 							"var jsonData = JSON.parse(responseBody);",
-							"tests[\"type is tenant\"] = jsonData.type === \"tenant\";"
+							"tests[\"type is tenant\"] = jsonData.type === \"tenant\";",
+							"",
+							"postman.setGlobalVariable(\"membrane_tenant\", jsonData.name);",
+							""
 						]
 					}
 				}
@@ -42,7 +70,7 @@
 			"response": []
 		},
 		{
-			"name": "Get my-tenant",
+			"name": "Get tenant info",
 			"event": [
 				{
 					"listen": "test",
@@ -58,7 +86,7 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant",
+				"url": "{{baseUrl}}/{{membrane_tenant}}",
 				"method": "GET",
 				"header": [],
 				"body": {},
@@ -67,7 +95,7 @@
 			"response": []
 		},
 		{
-			"name": "Create my-tenant/example-pojo",
+			"name": "Create data source - postgres",
 			"event": [
 				{
 					"listen": "test",
@@ -78,13 +106,16 @@
 							"",
 							"var jsonData = JSON.parse(responseBody);",
 							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
-							"tests[\"is updateable\"] = jsonData.updateable;"
+							"tests[\"is updateable\"] = jsonData.updateable;",
+							"",
+							"postman.setGlobalVariable(\"membrane_data_source\", jsonData.name);",
+							""
 						]
 					}
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant/example-pojo",
+				"url": "{{baseUrl}}/{{membrane_tenant}}/example-postgres",
 				"method": "PUT",
 				"header": [
 					{
@@ -95,14 +126,14 @@
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+					"raw": "{\n    \"type\":\"jdbc\",\n    \"url\": \"jdbc:postgresql://example-postgres/membrane\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
 				},
 				"description": ""
 			},
 			"response": []
 		},
 		{
-			"name": "Create my-tenant/example-postgres",
+			"name": "Create data source - couchdb",
 			"event": [
 				{
 					"listen": "test",
@@ -113,13 +144,16 @@
 							"",
 							"var jsonData = JSON.parse(responseBody);",
 							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
-							"tests[\"is updateable\"] = jsonData.updateable;"
+							"tests[\"is updateable\"] = jsonData.updateable;",
+							"",
+							"postman.setGlobalVariable(\"membrane_data_source\", jsonData.name);",
+							""
 						]
 					}
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant/example-postgres",
+				"url": "{{baseUrl}}/{{membrane_tenant}}/example-couchdb",
 				"method": "PUT",
 				"header": [
 					{
@@ -130,14 +164,14 @@
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"jdbc\",\n    \"url\": \"jdbc:postgresql://example-postgres/membrane\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
+					"raw": "{\n    \"type\":\"couchdb\",\n    \"hostname\": \"example-couchdb\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
 				},
 				"description": ""
 			},
 			"response": []
 		},
 		{
-			"name": "Create my-tenant/example-couchdb",
+			"name": "Create data source - pojo",
 			"event": [
 				{
 					"listen": "test",
@@ -148,13 +182,18 @@
 							"",
 							"var jsonData = JSON.parse(responseBody);",
 							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
-							"tests[\"is updateable\"] = jsonData.updateable;"
+							"tests[\"is updateable\"] = jsonData.updateable;",
+							"",
+							"postman.setGlobalVariable(\"membrane_data_source\", jsonData.name);",
+							"postman.setGlobalVariable(\"membrane_schema\", \"Schema\");",
+							"postman.setGlobalVariable(\"membrane_table\", \"foo\");",
+							""
 						]
 					}
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant/example-couchdb",
+				"url": "{{baseUrl}}/{{membrane_tenant}}/example-pojo",
 				"method": "PUT",
 				"header": [
 					{
@@ -165,14 +204,330 @@
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"couchdb\",\n    \"hostname\": \"example-couchdb\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
+					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Get data source info",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
+							"",
+							"// find the first non-information schema",
+							"var schemas = jsonData.schemas;",
+							"for (var i=0; i<schemas.length; i++) {",
+							"    if (schemas[i].name !== \"information_schema\") {",
+							"        postman.setGlobalVariable(\"membrane_schema\", schemas[i].name);",
+							"        break;",
+							"    }",
+							"}",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
 				},
 				"description": ""
 			},
 			"response": []
 		},
 		{
-			"name": "Delete my-tenant",
+			"name": "Get schema info",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is schema\"] = jsonData.type === \"schema\";",
+							"",
+							"// find the first non-information schema",
+							"var tables = jsonData.tables;",
+							"for (var i=0; i<tables.length; i++) {",
+							"    postman.setGlobalVariable(\"membrane_table\", tables[i].name);",
+							"    break;",
+							"}",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}",
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Get table info",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is table\"] = jsonData.type === \"table\";",
+							"",
+							"// find the first non-information schema",
+							"var columns = jsonData.columns;",
+							"for (var i=0; i<columns.length; i++) {",
+							"    postman.setGlobalVariable(\"membrane_column\", columns[i].name);",
+							"    break;",
+							"}",
+							"",
+							"postman.setGlobalVariable(\"membrane_column_count\", columns.length);",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}",
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Add table data",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"status is ok\"] = jsonData.status === \"ok\";",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
+				"method": "POST",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n\t\"insert\": [\n\t\t{\n\t\t\t\"bar\": 42,\n\t\t\t\"baz\": null\n\t\t}\n\t]\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Get table data",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is dataset\"] = jsonData.type === \"dataset\";",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Query table data",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is dataset\"] = jsonData.type === \"dataset\";",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/q?sql=SELECT * FROM {{membrane_table}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"q"
+					],
+					"query": [
+						{
+							"key": "sql",
+							"value": "SELECT * FROM {{membrane_table}}",
+							"equals": true,
+							"description": ""
+						}
+					],
+					"variable": []
+				},
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Get column info",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is column\"] = jsonData.type === \"column\";",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/c/{{membrane_column}}",
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Delete data source",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
+							"tests[\"deleted is true\"] = jsonData.deleted;",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
+				"method": "DELETE",
+				"header": [],
+				"body": {},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Delete tenant",
 			"event": [
 				{
 					"listen": "test",
@@ -190,7 +545,7 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant",
+				"url": "{{baseUrl}}/{{membrane_tenant}}",
 				"method": "DELETE",
 				"header": [],
 				"body": {},

http://git-wip-us.apache.org/repos/asf/metamodel-membrane/blob/b4abdb01/undertow/src/main/java/org/apache/metamodel/membrane/server/WebServer.java
----------------------------------------------------------------------
diff --git a/undertow/src/main/java/org/apache/metamodel/membrane/server/WebServer.java b/undertow/src/main/java/org/apache/metamodel/membrane/server/WebServer.java
index 1643bd8..4e3d188 100644
--- a/undertow/src/main/java/org/apache/metamodel/membrane/server/WebServer.java
+++ b/undertow/src/main/java/org/apache/metamodel/membrane/server/WebServer.java
@@ -22,6 +22,7 @@ import java.io.File;
 
 import javax.servlet.ServletException;
 
+import org.apache.metamodel.util.FileHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.web.context.ContextLoaderListener;
@@ -47,6 +48,8 @@ public class WebServer {
     private static final int DEFAULT_PORT = 8080;
 
     public static void main(final String[] args) throws Exception {
+        prepareDataDirectoryIfNeeded();
+
         final String portEnv = System.getenv("MEMBRANE_HTTP_PORT");
         final int port = Strings.isNullOrEmpty(portEnv) ? DEFAULT_PORT : Integer.parseInt(portEnv);
 
@@ -57,6 +60,15 @@ public class WebServer {
         logger.info("Apache MetaModel Membrane server started on port {}", port);
     }
 
+    private static void prepareDataDirectoryIfNeeded() {
+        final String property = "DATA_DIRECTORY";
+        if (System.getProperty(property) == null && System.getenv(property) == null) {
+            final String tempDirectory = FileHelper.getTempDir().getAbsolutePath() + "/membrane";
+            logger.warn("No DATA_DIRECTORY defined, using {}", tempDirectory);
+            System.setProperty(property, tempDirectory);
+        }
+    }
+
     public static void startServer(int port) throws Exception {
         final DeploymentInfo deployment = Servlets.deployment().setClassLoader(WebServer.class.getClassLoader());
         deployment.setContextPath("");