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>