Merge branch 'master' into METAMODEL-1153

Conflicts:
	core/src/main/java/org/apache/metamodel/membrane/app/CachedDataSourceRegistryWrapper.java
	core/src/main/java/org/apache/metamodel/membrane/app/DataSourceRegistry.java
	core/src/main/java/org/apache/metamodel/membrane/app/InMemoryDataSourceRegistry.java
	core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
	core/src/main/java/org/apache/metamodel/membrane/controllers/DataSourceController.java
	core/src/main/resources/swagger.yaml
diff --git a/undertow/Dockerfile b/.dockerignore
similarity index 79%
rename from undertow/Dockerfile
rename to .dockerignore
index 371d24b..3eddedc 100644
--- a/undertow/Dockerfile
+++ b/.dockerignore
@@ -15,12 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-FROM openjdk:8-jre-alpine
-
-COPY target/membrane-undertow-server.jar membrane-undertow-server.jar
-
-VOLUME /data
-
-ENV DATA_DIRECTORY=/data
-
-CMD java -server -jar membrane-undertow-server.jar
+**/target
diff --git a/.travis.yml b/.travis.yml
index f78ad69..948b691 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,18 @@
 sudo: false
 language: java
 jdk:
-  - oraclejdk8
+- oraclejdk8
 
-after_success:
-  - mvn test javadoc:javadoc
+services:
+- docker
+
+before_install:
+- sudo apt-get -qq update
+- sudo apt-get install npm
+- sudo npm install newman --global;
+
+script:
+- mvn clean install -Pfull
 
 notifications:
   email: false
\ No newline at end of file
diff --git a/undertow/Dockerfile b/Dockerfile
similarity index 77%
copy from undertow/Dockerfile
copy to Dockerfile
index 371d24b..a10b5d8 100644
--- a/undertow/Dockerfile
+++ b/Dockerfile
@@ -15,12 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
-FROM openjdk:8-jre-alpine
+FROM maven:3.5-jdk-8-alpine
 
-COPY target/membrane-undertow-server.jar membrane-undertow-server.jar
-
+# Set data directory used for the app's persistence
 VOLUME /data
-
 ENV DATA_DIRECTORY=/data
 
-CMD java -server -jar membrane-undertow-server.jar
+COPY . /usr/src/app
+WORKDIR /usr/src/app
+
+RUN mvn clean install -Pdockerbuild -DskipTests
+
+CMD java -server -jar undertow/target/membrane-undertow-server.jar
diff --git a/core/pom.xml b/core/pom.xml
index c46c699..2de9377 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -86,6 +86,18 @@
 			<groupId>org.slf4j</groupId>
 			<artifactId>jcl-over-slf4j</artifactId>
 		</dependency>
+		
+		<!-- Database drivers -->
+		<dependency>
+			<groupId>org.postgresql</groupId>
+			<artifactId>postgresql</artifactId>
+			<version>42.1.4</version>
+		</dependency>
+		<dependency>
+			<groupId>net.sourceforge.jtds</groupId>
+			<artifactId>jtds</artifactId>
+			<version>1.3.1</version>
+		</dependency>
 
 		<!-- Provided -->
 		<dependency>
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/CachedDataSourceRegistryWrapper.java b/core/src/main/java/org/apache/metamodel/membrane/app/CachedDataSourceRegistryWrapper.java
index 96c9e80..b7b5b1e 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/CachedDataSourceRegistryWrapper.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/CachedDataSourceRegistryWrapper.java
@@ -36,14 +36,13 @@
 import com.google.common.util.concurrent.UncheckedExecutionException;
 
 /**
- * A wrapper that adds a cache around a {@link DataSourceRegistry} in order to
- * prevent re-connecting all the time to the same data source.
+ * A wrapper that adds a cache around a {@link DataSourceRegistry} in order to prevent re-connecting all the time to the
+ * same data source.
  */
 public class CachedDataSourceRegistryWrapper implements DataSourceRegistry {
 
     /**
-     * The default timeout (in seconds) before the cache evicts and closes the
-     * created {@link DataContext}s.
+     * The default timeout (in seconds) before the cache evicts and closes the created {@link DataContext}s.
      */
     public static final int DEFAULT_TIMEOUT_SECONDS = 60;
 
@@ -57,8 +56,8 @@
     public CachedDataSourceRegistryWrapper(final DataSourceRegistry delegate, final long cacheTimeout,
             final TimeUnit cacheTimeoutUnit) {
         this.delegate = delegate;
-        this.loadingCache = CacheBuilder.newBuilder().expireAfterAccess(cacheTimeout, cacheTimeoutUnit).removalListener(
-                createRemovalListener()).build(createCacheLoader());
+        this.loadingCache = CacheBuilder.newBuilder().expireAfterAccess(cacheTimeout, cacheTimeoutUnit)
+                .removalListener(createRemovalListener()).build(createCacheLoader());
     }
 
     private RemovalListener<String, DataContext> createRemovalListener() {
@@ -102,8 +101,8 @@
             if (cause instanceof RuntimeException) {
                 throw (RuntimeException) cause;
             }
-            throw new MetaModelException("Unexpected error happened while getting DataContext '" + dataSourceName
-                    + "' from cache", e);
+            throw new MetaModelException(
+                    "Unexpected error happened while getting DataContext '" + dataSourceName + "' from cache", e);
         }
     }
 
@@ -112,4 +111,9 @@
         delegate.removeDataSource(dataSourceName);
         loadingCache.invalidate(dataSourceName);
     }
+    
+    @Override
+    public DataContext openDataContext(DataContextProperties properties) {
+        return delegate.openDataContext(properties);
+    }
 }
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/DataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/DataSourceRegistry.java
index 53ba51d..1c5db17 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/DataSourceRegistry.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/DataSourceRegistry.java
@@ -34,13 +34,45 @@
 
     public List<String> getDataSourceNames();
 
-    public String registerDataSource(String dataSourceName, DataContextProperties dataContextProperties) throws DataSourceAlreadyExistException;
+    /**
+     * 
+     * @param dataSourceName
+     * @param dataContextProperties
+     * @return the identifier/name for the data source.
+     * @throws DataSourceAlreadyExistException
+     */
+    public String registerDataSource(String dataSourceName, DataContextProperties dataContextProperties)
+            throws DataSourceAlreadyExistException;
 
     public void removeDataSource(String dataSourceName) throws NoSuchDataSourceException;
     
+    /**
+     * Opens a {@link DataContext} that exists in the registry.
+     * 
+     * @param dataSourceName
+     * @return
+     * @throws NoSuchDataSourceException
+     */
     public DataContext openDataContext(String dataSourceName) throws NoSuchDataSourceException;
 
-    public default UpdateableDataContext openDataContextForUpdate(String dataSourceName) {
+    /**
+     * Opens a {@link DataContext} based on a set of {@link DataContextProperties}. This allows you to instantiate a
+     * data source without necesarily having registered it (yet).
+     * 
+     * @param properties
+     * @return
+     */
+    public DataContext openDataContext(DataContextProperties properties);
+
+    /**
+     * Opens a {@link UpdateableDataContext} that exists in the registry.
+     * 
+     * @param dataSourceName
+     * @return
+     * @throws DataSourceNotUpdateableException
+     */
+    public default UpdateableDataContext openDataContextForUpdate(String dataSourceName)
+            throws DataSourceNotUpdateableException {
         final DataContext dataContext = openDataContext(dataSourceName);
         if (dataContext instanceof UpdateableDataContext) {
             return (UpdateableDataContext) dataContext;
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/InMemoryDataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/InMemoryDataSourceRegistry.java
index 573dfd7..338ed86 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/InMemoryDataSourceRegistry.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/InMemoryDataSourceRegistry.java
@@ -70,4 +70,8 @@
         dataSources.remove(dataSourceName);
     }
 
+    public DataContext openDataContext(DataContextProperties properties) {
+        final DataContextSupplier supplier = new DataContextSupplier(null, properties);
+        return supplier.get();
+    }
 }
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/exceptions/InvalidDataSourceException.java b/core/src/main/java/org/apache/metamodel/membrane/app/exceptions/InvalidDataSourceException.java
new file mode 100644
index 0000000..4cdfffb
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/exceptions/InvalidDataSourceException.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.metamodel.membrane.app.exceptions;
+
+import org.apache.metamodel.MetaModelException;
+
+public class InvalidDataSourceException extends MetaModelException {
+
+    private static final long serialVersionUID = 1L;
+
+    public InvalidDataSourceException(Exception cause) {
+        super(cause.getClass().getSimpleName() + (cause.getMessage() == null ? "" : ": " + cause.getMessage()));
+    }
+}
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
index ba1506c..1a7759d 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
@@ -135,4 +135,8 @@
         }
     }
 
+    public DataContext openDataContext(DataContextProperties properties) {
+        final DataContextSupplier supplier = new DataContextSupplier(null, properties);
+        return supplier.get();
+    }
 }
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 ef1fe67..1efcfed 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
@@ -33,6 +33,7 @@
 import org.apache.metamodel.membrane.app.DataSourceRegistry;
 import org.apache.metamodel.membrane.app.TenantContext;
 import org.apache.metamodel.membrane.app.TenantRegistry;
+import org.apache.metamodel.membrane.app.exceptions.InvalidDataSourceException;
 import org.apache.metamodel.membrane.controllers.model.RestDataSourceDefinition;
 import org.apache.metamodel.membrane.swagger.model.DeleteDatasourceResponse;
 import org.apache.metamodel.membrane.swagger.model.GetDatasourceResponse;
@@ -45,6 +46,7 @@
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -65,21 +67,28 @@
     @ResponseBody
     public GetDatasourceResponse put(@PathVariable("tenant") String tenantId,
             @PathVariable("datasource") String dataSourceId,
-            @Valid @RequestBody RestDataSourceDefinition dataContextDefinition) {
+            @RequestParam(value = "validate", required = false) Boolean validate,
+            @Valid @RequestBody RestDataSourceDefinition dataSourceDefinition) {
 
         final Map<String, Object> map = new HashMap<>();
-        map.putAll(dataContextDefinition.getProperties());
-        map.put(DataContextPropertiesImpl.PROPERTY_DATA_CONTEXT_TYPE, dataContextDefinition.getType());
-
-        if (!map.containsKey(DataContextPropertiesImpl.PROPERTY_DATABASE)) {
-            // add the data source ID as database name if it is not already set.
-            map.put(DataContextPropertiesImpl.PROPERTY_DATABASE, dataSourceId);
-        }
+        map.putAll(dataSourceDefinition.getProperties());
+        map.put(DataContextPropertiesImpl.PROPERTY_DATA_CONTEXT_TYPE, dataSourceDefinition.getType());
 
         final DataContextProperties properties = new DataContextPropertiesImpl(map);
 
-        final String dataSourceIdentifier = tenantRegistry.getTenantContext(tenantId).getDataSourceRegistry()
-                .registerDataSource(dataSourceId, properties);
+        final DataSourceRegistry dataSourceRegistry = tenantRegistry.getTenantContext(tenantId).getDataSourceRegistry();
+        if (validate != null && validate.booleanValue()) {
+            // validate the data source by opening it and ensuring that a basic call such as getDefaultSchema() works.
+            try {
+                final DataContext dataContext = dataSourceRegistry.openDataContext(properties);
+                dataContext.getDefaultSchema();
+            } catch (Exception e) {
+                logger.warn("Failed validation for PUT datasource '{}/{}'", tenantId, dataSourceId, e);
+                throw new InvalidDataSourceException(e);
+            }
+        }
+
+        final String dataSourceIdentifier = dataSourceRegistry.registerDataSource(dataSourceId, properties);
 
         logger.info("Created data source: {}/{}", tenantId, dataSourceIdentifier);
 
@@ -91,21 +100,31 @@
     public GetDatasourceResponse get(@PathVariable("tenant") String tenantId,
             @PathVariable("datasource") String dataSourceName) {
         final TenantContext tenantContext = tenantRegistry.getTenantContext(tenantId);
-        final DataContext dataContext = tenantContext.getDataSourceRegistry().openDataContext(dataSourceName);
 
         final String tenantName = tenantContext.getTenantName();
         final UriBuilder uriBuilder = UriBuilder.fromPath("/{tenant}/{dataContext}/s/{schema}");
 
-        final List<GetDatasourceResponseSchemas> schemaLinks = dataContext.getSchemaNames().stream().map(s -> {
-            final String uri = uriBuilder.build(tenantName, dataSourceName, s).toString();
-            return new GetDatasourceResponseSchemas().name(s).uri(uri);
-        }).collect(Collectors.toList());
+        List<GetDatasourceResponseSchemas> schemaLinks;
+        Boolean updateable;
+        try {
+            final DataContext dataContext = tenantContext.getDataSourceRegistry().openDataContext(dataSourceName);
+            updateable = dataContext instanceof UpdateableDataContext;
+            schemaLinks = dataContext.getSchemaNames().stream().map(s -> {
+                final String uri = uriBuilder.build(tenantName, dataSourceName, s).toString();
+                return new GetDatasourceResponseSchemas().name(s).uri(uri);
+            }).collect(Collectors.toList());
+        } catch (Exception e) {
+            logger.warn("Failed to open for GET datasource '{}/{}'. No schemas will be listed.", tenantId,
+                    dataSourceName, e);
+            updateable = null;
+            schemaLinks = null;
+        }
 
         final GetDatasourceResponse resp = new GetDatasourceResponse();
         resp.type("datasource");
         resp.name(dataSourceName);
         resp.tenant(tenantName);
-        resp.updateable(dataContext instanceof UpdateableDataContext);
+        resp.updateable(updateable);
         resp.queryUri(
                 UriBuilder.fromPath("/{tenant}/{dataContext}/query").build(tenantName, dataSourceName).toString());
         resp.schemas(schemaLinks);
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 5d79f56..1b5ec4d 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
@@ -26,6 +26,7 @@
 import org.apache.metamodel.membrane.app.exceptions.AbstractIdentifierNamingException;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceAlreadyExistException;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceNotUpdateableException;
+import org.apache.metamodel.membrane.app.exceptions.InvalidDataSourceException;
 import org.apache.metamodel.membrane.app.exceptions.NoSuchColumnException;
 import org.apache.metamodel.membrane.app.exceptions.NoSuchDataSourceException;
 import org.apache.metamodel.membrane.app.exceptions.NoSuchSchemaException;
@@ -129,6 +130,21 @@
         return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "DataSource not updateable: " + ex
                 .getDataSourceName());
     }
+    
+    /**
+     * DataSource invalid exception handler method - mapped to
+     * BAD_REQUEST.
+     * 
+     * @param ex
+     * @return
+     */
+    @ExceptionHandler(InvalidDataSourceException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ResponseBody
+    public RestErrorResponse processDataSourceNotUpdateable(InvalidDataSourceException ex) {
+        return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(), "DataSource invalid: " + ex
+                .getMessage());
+    }
 
     /**
      * Query parsing exception - mapped to BAD_REQUEST.
diff --git a/core/src/main/java/org/apache/metamodel/membrane/controllers/model/RestDataSourceDefinition.java b/core/src/main/java/org/apache/metamodel/membrane/controllers/model/RestDataSourceDefinition.java
index eae3dc6..b3ac4ec 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/controllers/model/RestDataSourceDefinition.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/controllers/model/RestDataSourceDefinition.java
@@ -59,6 +59,10 @@
     public String getType() {
         return type;
     }
+    
+    public void setType(String type) {
+        this.type = type;
+    }
 
     @JsonAnyGetter
     @Override
diff --git a/core/src/main/resources/swagger.yaml b/core/src/main/resources/swagger.yaml
index e15b60e..37b64f8 100644
--- a/core/src/main/resources/swagger.yaml
+++ b/core/src/main/resources/swagger.yaml
@@ -105,12 +105,18 @@
             $ref: "#/definitions/error"
     put:
       parameters:
+        - name: validate
+          in: query
+          description: Whether or not to validate the datasource properties and reject creating the datasource if it cannot be created.
+          required: false
+          type: boolean
+          default: false
         - name: inputData
           in: body
           description: The definition of the datasource using properties. The same properties as normally applied in MetaModel factories (e.g. 'type', 'resource', 'url', 'driver-class' ,'hostname', 'port', 'catalog', 'database', 'username', 'port', 'table-defs') are used here.
           required: true
           schema:
-            $ref: "#/definitions/putDatasourceResponse"
+            $ref: "#/definitions/putDatasourceRequest"
       responses:
         200:
           description: Datasource created
@@ -124,6 +130,8 @@
             $ref: "#/definitions/deleteDatasourceResponse"
         404:
           description: Datasource not found
+        400:
+          description: Datasource validation failed
           schema:
             $ref: "#/definitions/error"
   /{tenant}/{datasource}/q:
@@ -509,7 +517,7 @@
         description: An array of generated keys for the records, if any
         items:
           type: object
-  putDatasourceResponse:
+  putDatasourceRequest:
     type: object
     properties:
       type:
diff --git a/core/src/test/java/org/apache/metamodel/membrane/controllers/DataSourceControllerTest.java b/core/src/test/java/org/apache/metamodel/membrane/controllers/DataSourceControllerTest.java
new file mode 100644
index 0000000..2a7e240
--- /dev/null
+++ b/core/src/test/java/org/apache/metamodel/membrane/controllers/DataSourceControllerTest.java
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.metamodel.membrane.controllers;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.metamodel.membrane.app.InMemoryTenantRegistry;
+import org.apache.metamodel.membrane.app.TenantRegistry;
+import org.apache.metamodel.membrane.app.exceptions.InvalidDataSourceException;
+import org.apache.metamodel.membrane.controllers.model.RestDataSourceDefinition;
+import org.apache.metamodel.membrane.swagger.model.GetDatasourceResponse;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class DataSourceControllerTest {
+
+    private final TenantRegistry tenantRegistry = new InMemoryTenantRegistry();
+    private final DataSourceController dataSourceController = new DataSourceController(tenantRegistry);
+
+    @Rule
+    public TestName name = new TestName();
+
+    @Test
+    public void testPutWithoutValidation() throws Exception {
+        final String tenant = name.getMethodName();
+        tenantRegistry.createTenantContext(tenant);
+
+        final RestDataSourceDefinition dataSourceDefinition = new RestDataSourceDefinition();
+        dataSourceDefinition.setType("foo bar");
+        dataSourceDefinition.set("hello", "world");
+
+        final GetDatasourceResponse resp = dataSourceController.put(tenant, "ds1", false, dataSourceDefinition);
+        assertEquals("ds1", resp.getName());
+        assertEquals(null, resp.getSchemas());
+        assertEquals(null, resp.getUpdateable());
+    }
+
+    @Test
+    public void testPutWithValidationFailing() throws Exception {
+        final String tenant = name.getMethodName();
+        tenantRegistry.createTenantContext(tenant);
+
+        final RestDataSourceDefinition dataSourceDefinition = new RestDataSourceDefinition();
+        dataSourceDefinition.setType("foo bar");
+        dataSourceDefinition.set("hello", "world");
+
+        try {
+            dataSourceController.put(tenant, "ds1", true, dataSourceDefinition);
+            Assert.fail("exception expected");
+        } catch (InvalidDataSourceException e) {
+            assertEquals("UnsupportedDataContextPropertiesException", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testPutWithValidationPassing() throws Exception {
+        final String tenant = name.getMethodName();
+        tenantRegistry.createTenantContext(tenant);
+
+        final RestDataSourceDefinition dataSourceDefinition = new RestDataSourceDefinition();
+        dataSourceDefinition.setType("pojo");
+
+        final GetDatasourceResponse resp = dataSourceController.put(tenant, "ds1", true, dataSourceDefinition);
+
+        final ObjectMapper objectMapper = new ObjectMapper();
+
+        assertEquals(
+                "[{'name':'information_schema','uri':'/testPutWithValidationPassing/ds1/s/information_schema'},"
+                        + "{'name':'Schema','uri':'/testPutWithValidationPassing/ds1/s/Schema'}]",
+                objectMapper.writeValueAsString(resp.getSchemas()).replace('\"', '\''));
+    }
+
+}
diff --git a/core/src/test/java/org/apache/metamodel/membrane/controllers/TenantInteractionScenarioTest.java b/core/src/test/java/org/apache/metamodel/membrane/controllers/TenantInteractionScenarioTest.java
index b410e01..3e0575d 100644
--- a/core/src/test/java/org/apache/metamodel/membrane/controllers/TenantInteractionScenarioTest.java
+++ b/core/src/test/java/org/apache/metamodel/membrane/controllers/TenantInteractionScenarioTest.java
@@ -48,14 +48,14 @@
 
         final TenantRegistry tenantRegistry = new InMemoryTenantRegistry();
         final TenantController tenantController = new TenantController(tenantRegistry);
-        final DataSourceController dataContextController = new DataSourceController(tenantRegistry);
+        final DataSourceController dataSourceController = new DataSourceController(tenantRegistry);
         final SchemaController schemaController = new SchemaController(tenantRegistry);
         final TableController tableController = new TableController(tenantRegistry);
         final ColumnController columnController = new ColumnController(tenantRegistry);
         final QueryController queryController = new QueryController(tenantRegistry);
         final TableDataController tableDataController = new TableDataController(tenantRegistry);
 
-        mockMvc = MockMvcBuilders.standaloneSetup(tenantController, dataContextController, schemaController,
+        mockMvc = MockMvcBuilders.standaloneSetup(tenantController, dataSourceController, schemaController,
                 tableController, columnController, queryController, tableDataController).setMessageConverters(
                         messageConverter).build();
     }
@@ -77,7 +77,7 @@
         // create datasource
         {
             final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.put("/tenant1/mydata").content(
-                    "{'type':'pojo','table-defs':'hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);'}"
+                    "{'type':'pojo','database':'mydata','table-defs':'hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);'}"
                             .replace('\'', '"')).contentType(MediaType.APPLICATION_JSON)).andExpect(
                                     MockMvcResultMatchers.status().isOk()).andReturn();
 
@@ -185,8 +185,8 @@
         // query metadata from information_schema
         {
             final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/q?sql={sql}",
-                    "SELECT greeting, who AS who_is_it FROM hello_world")).andExpect(MockMvcResultMatchers.status()
-                            .isOk()).andReturn();
+                    "SELECT greeting, who AS who_is_it FROM hello_world"))
+                    .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
 
             final String content = result.getResponse().getContentAsString();
             final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class);
diff --git a/docker-compose.yml b/docker-compose.yml
index c87a2bb..fc117df 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,8 +19,26 @@
   metamodel-membrane:
     container_name: metamodel-membrane
     image: metamodel-membrane
-    build: undertow
+    build: .
     ports:
     - "8080:8080"
     environment:
     - MEMBRANE_HTTP_PORT=8080
+    depends_on:
+    - example-postgres
+    - example-couchdb
+  example-postgres:
+    container_name: example-postgres
+    image: postgres:9.6
+    environment:
+    - POSTGRES_USER=membrane
+    - POSTGRES_PASSWORD=secret
+    - POSTGRES_DB=membrane
+  example-couchdb:
+    container_name: example-couchdb
+    image: couchdb:1.6
+    environment:
+    - COUCHDB_USER=membrane
+    - COUCHDB_PASSWORD=secret
+    ports:
+    - 5984:5984
diff --git a/pom.xml b/pom.xml
index 4b43522..1bd76c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,12 +42,87 @@
 	<url>http://metamodel.apache.org</url>
 	<inceptionYear>2017</inceptionYear>
 	<packaging>pom</packaging>
-	<modules>
-		<module>swagger-codegen</module>
-		<module>core</module>
-		<module>war</module>
-		<module>undertow</module>
-	</modules>
+
+	<profiles>
+		<profile>
+			<!-- Normal profile for a build on a machine without specific tools like Docker or Newman installed -->
+			<id>normal</id>
+			<activation>
+				<activeByDefault>true</activeByDefault>
+			</activation>
+			<modules>
+				<module>swagger-codegen</module>
+				<module>core</module>
+				<module>war</module>
+				<module>undertow</module>
+			</modules>
+		</profile>
+		<profile>
+			<!-- Minimal profile for running just the docker image build -->
+			<id>dockerbuild</id>
+			<modules>
+				<module>swagger-codegen</module>
+				<module>core</module>
+				<module>undertow</module>
+			</modules>
+		</profile>
+		<profile>
+			<!-- Build profile for running everything - requires docker and newman installed -->
+			<id>full</id>
+			<modules>
+				<module>swagger-codegen</module>
+				<module>core</module>
+				<module>war</module>
+				<module>undertow</module>
+				<module>postman-tests</module>
+			</modules>
+		</profile>
+	</profiles>
+
+	<build>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.apache.rat</groupId>
+					<artifactId>apache-rat-plugin</artifactId>
+					<configuration>
+						<licenses>
+							<license
+								implementation="org.apache.rat.analysis.license.SimplePatternBasedLicense">
+								<licenseFamilyCategory>ASL20</licenseFamilyCategory>
+								<licenseFamilyName>Apache Software License, 2.0</licenseFamilyName>
+								<notes>Single licensed ASL v2.0</notes>
+								<patterns>
+									<pattern>Licensed to the Apache Software Foundation (ASF) under
+										one
+										or more contributor license agreements.</pattern>
+								</patterns>
+							</license>
+						</licenses>
+						<excludeSubProjects>false</excludeSubProjects>
+						<excludes>
+							<exclude>KEYS</exclude>
+							<exclude>**/*.md</exclude>
+							<exclude>**/*.json</exclude>
+							<exclude>**/.gitignore/**</exclude>
+							<exclude>.git/**</exclude>
+							<exclude>.gitattributes</exclude>
+							<exclude>**/.project</exclude>
+							<exclude>**/.classpath</exclude>
+							<exclude>**/.settings/**</exclude>
+							<exclude>**/.vscode/**</exclude>
+							<exclude>**/.travis.yml</exclude>
+							<exclude>**/target/**</exclude>
+							<exclude>**/*.iml/**</exclude>
+							<exclude>**/*.iws/**</exclude>
+							<exclude>**/*.ipr/**</exclude>
+							<exclude>**/.idea/**</exclude>
+						</excludes>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
 
 	<dependencyManagement>
 		<dependencies>
diff --git a/postman-tests/Membrane.postman_collection.json b/postman-tests/Membrane.postman_collection.json
new file mode 100644
index 0000000..d8f0896
--- /dev/null
+++ b/postman-tests/Membrane.postman_collection.json
@@ -0,0 +1,202 @@
+{
+	"variables": [],
+	"info": {
+		"name": "Membrane",
+		"_postman_id": "084c91ce-fccc-8fc4-a3c4-1dc5d5b41388",
+		"description": "",
+		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
+	},
+	"item": [
+		{
+			"name": "Create my-tenant",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is tenant\"] = jsonData.type === \"tenant\";"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant",
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Get my-tenant",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is tenant\"] = jsonData.type === \"tenant\";"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant",
+				"method": "GET",
+				"header": [],
+				"body": {},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Create my-tenant/example-pojo",
+			"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[\"is updateable\"] = jsonData.updateable;"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant/example-pojo",
+				"method": "PUT",
+				"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": "Create my-tenant/example-postgres",
+			"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[\"is updateable\"] = jsonData.updateable;"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant/example-postgres",
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"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-couchdb",
+			"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[\"is updateable\"] = jsonData.updateable;"
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant/example-couchdb",
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json",
+						"description": ""
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\":\"couchdb\",\n    \"hostname\": \"example-couchdb\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
+				},
+				"description": ""
+			},
+			"response": []
+		},
+		{
+			"name": "Delete my-tenant",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"type": "text/javascript",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is tenant\"] = jsonData.type === \"tenant\";",
+							"tests[\"deleted is true\"] = jsonData.deleted;",
+							""
+						]
+					}
+				}
+			],
+			"request": {
+				"url": "{{baseUrl}}/my-tenant",
+				"method": "DELETE",
+				"header": [],
+				"body": {},
+				"description": ""
+			},
+			"response": []
+		}
+	]
+}
\ No newline at end of file
diff --git a/postman-tests/README.md b/postman-tests/README.md
new file mode 100644
index 0000000..0f69277
--- /dev/null
+++ b/postman-tests/README.md
@@ -0,0 +1,20 @@
+# Membrane Postman tests
+
+This folder has a set of tests and examples for use with the [Postman](https://www.getpostman.com/) tool.
+
+## Environment files
+
+The environments folder contains a set of reusable environment configs. Creating your own custom environment file should also be trivial since we only currently have one environment variable: `baseUrl`. 
+
+## Prerequisites
+
+The tests are designed to work with the default `docker-compose.yml` file in the root of the membrane project. This compose file has some databases and such which are used as part of the tests.
+
+## Running from command line
+
+If you have Postman's CLI tool `newman` installed, simply go like this:
+
+```
+newman run -e environments/docker-toolbox.postman_environment.json Membrane.postman_collection.json
+```
+(using the `docker-toolbox` environment)
diff --git a/postman-tests/environments/docker-toolbox.postman_environment.json b/postman-tests/environments/docker-toolbox.postman_environment.json
new file mode 100644
index 0000000..e04cb2e
--- /dev/null
+++ b/postman-tests/environments/docker-toolbox.postman_environment.json
@@ -0,0 +1,16 @@
+{
+  "id": "556c1331-5e87-4201-2767-8089cdb8e523",
+  "name": "Membrane docker-toolbox",
+  "values": [
+    {
+      "enabled": true,
+      "key": "baseUrl",
+      "value": "http://192.168.99.100:8080",
+      "type": "text"
+    }
+  ],
+  "timestamp": 1503214232219,
+  "_postman_variable_scope": "environment",
+  "_postman_exported_at": "2017-08-20T08:06:07.385Z",
+  "_postman_exported_using": "Postman/5.1.3"
+}
\ No newline at end of file
diff --git a/postman-tests/environments/localhost.postman_environment.json b/postman-tests/environments/localhost.postman_environment.json
new file mode 100644
index 0000000..527d430
--- /dev/null
+++ b/postman-tests/environments/localhost.postman_environment.json
@@ -0,0 +1,16 @@
+{
+  "id": "fa99af7e-2115-254e-649e-7a2742704dbb",
+  "name": "Membrane localhost",
+  "values": [
+    {
+      "enabled": true,
+      "key": "baseUrl",
+      "value": "http://localhost:8080",
+      "type": "text"
+    }
+  ],
+  "timestamp": 1503066492942,
+  "_postman_variable_scope": "environment",
+  "_postman_exported_at": "2017-08-20T08:06:12.977Z",
+  "_postman_exported_using": "Postman/5.1.3"
+}
\ No newline at end of file
diff --git a/postman-tests/pom.xml b/postman-tests/pom.xml
new file mode 100644
index 0000000..2d4c04c
--- /dev/null
+++ b/postman-tests/pom.xml
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<parent>
+		<groupId>org.apache.metamodel.membrane</groupId>
+		<artifactId>Membrane-parent</artifactId>
+		<version>0.1-SNAPSHOT</version>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+	<artifactId>Membrane-postman-tests</artifactId>
+	<packaging>pom</packaging>
+	
+	<properties>
+		<compose.args>--build</compose.args>
+	</properties>
+
+	<build>
+		<plugins>
+			<plugin>
+				<!-- Add the docker-maven-plugin to make the "${docker.host.address}" 
+					variable resolveable -->
+				<groupId>io.fabric8</groupId>
+				<artifactId>docker-maven-plugin</artifactId>
+				<version>0.21.0</version>
+				<executions>
+					<execution>
+						<id>start-docker-on-pre-integration-test</id>
+						<goals>
+							<goal>start</goal>
+						</goals>
+						<phase>pre-integration-test</phase>
+					</execution>
+				</executions>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-antrun-plugin</artifactId>
+				<version>1.8</version>
+				<executions>
+					<execution>
+						<id>determine-postman-env</id>
+						<phase>pre-integration-test</phase>
+						<goals>
+							<goal>run</goal>
+						</goals>
+						<configuration>
+							<!-- Use ant to determine the right value for 'postman.env' -->
+							<exportAntProperties>true</exportAntProperties>
+							<target>
+								<condition property="postman.env" value="docker-toolbox"
+									else="localhost">
+									<equals arg1="${docker.host.address}" arg2="192.168.99.100" />
+								</condition>
+								<echo message="Using postman environment: ${postman.env}" />
+							</target>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>exec-maven-plugin</artifactId>
+				<version>1.5.0</version>
+				<executions>
+					<execution>
+						<id>docker-compose-up</id>
+						<phase>pre-integration-test</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<executable>docker-compose</executable>
+							<commandlineArgs>
+							<![CDATA[
+							-f ../docker-compose.yml up -d ${compose.args}
+							]]>
+							</commandlineArgs>
+						</configuration>
+					</execution>
+
+					<execution>
+						<id>newman-test</id>
+						<phase>integration-test</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<skip>${skipTests}</skip>
+							<executable>bash</executable>
+							<commandlineArgs>
+							<![CDATA[
+			                -c "newman run ./Membrane.postman_collection.json \
+			                -e ./environments/${postman.env}.postman_environment.json \
+			                --reporters junit,cli -x \
+			                --reporter-junit-export ./target/NewmanTestResults.xml"
+			                ]]>
+							</commandlineArgs>
+						</configuration>
+					</execution>
+
+					<execution>
+						<id>docker-logs</id>
+						<phase>post-integration-test</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<executable>bash</executable>
+							<commandlineArgs>
+				            <![CDATA[
+				            -c "docker logs metamodel-membrane > target/docker-membrane.log"
+				            ]]>
+							</commandlineArgs>
+						</configuration>
+					</execution>
+					
+					<execution>
+						<id>docker-compose-down</id>
+						<phase>post-integration-test</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<executable>docker-compose</executable>
+							<commandlineArgs>
+				            <![CDATA[
+				            -f ../docker-compose.yml down
+				            ]]>
+							</commandlineArgs>
+						</configuration>
+					</execution>
+
+					<execution>
+						<id>newman-verify</id>
+						<phase>verify</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<skip>${skipTests}</skip>
+							<executable>grep</executable>
+							<commandlineArgs>
+				            <![CDATA[
+				            -q 'error\|failure' ./target/NewmanTestResults.xml
+				            ]]>
+							</commandlineArgs>
+							<successCodes>
+								<successCode>1</successCode>
+							</successCodes>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+
+			<plugin>
+				<groupId>org.kuali.maven.plugins</groupId>
+				<artifactId>maven-http-plugin</artifactId>
+				<version>1.0.5</version>
+
+				<configuration>
+					<httpSuccessCodes>200</httpSuccessCodes>
+					<httpContinueWaitingCodes>500</httpContinueWaitingCodes>
+					<requestTimeout>12000</requestTimeout>
+					<sleepInterval>4000</sleepInterval>
+					<timeout>120000</timeout>
+				</configuration>
+
+				<executions>
+					<execution>
+						<id>membrane-health-check</id>
+						<phase>pre-integration-test</phase>
+						<goals>
+							<goal>wait</goal>
+						</goals>
+						<configuration>
+							<url>http://${docker.host.address}:8080</url>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+
+		</plugins>
+	</build>
+</project>