Added ability to query data and to specify POJO table defs. Project: http://git-wip-us.apache.org/repos/asf/metamodel/repo Commit: http://git-wip-us.apache.org/repos/asf/metamodel/commit/8d4d0351 Tree: http://git-wip-us.apache.org/repos/asf/metamodel/tree/8d4d0351 Diff: http://git-wip-us.apache.org/repos/asf/metamodel/diff/8d4d0351
Branch: refs/heads/feature/5.x/swagger-docs Commit: 8d4d03519d56ce482139ea42fb178504e288c892 Parents: 8722c19 Author: Kasper Sørensen <[email protected]> Authored: Wed Jun 8 02:35:20 2016 -0700 Committer: Kasper Sørensen <[email protected]> Committed: Wed Jun 8 02:39:27 2016 -0700 ---------------------------------------------------------------------- .../service/app/DataContextSupplier.java | 72 ++++++++++++++++++ .../service/app/DataSourceDefinition.java | 6 ++ .../service/app/InMemoryDataSourceRegistry.java | 24 +++--- .../service/controllers/QueryController.java | 79 ++++++++++++++++++++ .../model/RestDataSourceDefinition.java | 25 ++++++- .../TenantInteractionScenarioTest.java | 66 ++++++++-------- 6 files changed, 226 insertions(+), 46 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/main/java/org/apache/metamodel/service/app/DataContextSupplier.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/main/java/org/apache/metamodel/service/app/DataContextSupplier.java b/service-webapp/src/main/java/org/apache/metamodel/service/app/DataContextSupplier.java new file mode 100644 index 0000000..3b36e2b --- /dev/null +++ b/service-webapp/src/main/java/org/apache/metamodel/service/app/DataContextSupplier.java @@ -0,0 +1,72 @@ +/** + * 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.service.app; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.metamodel.DataContext; +import org.apache.metamodel.pojo.ArrayTableDataProvider; +import org.apache.metamodel.pojo.PojoDataContext; +import org.apache.metamodel.pojo.TableDataProvider; +import org.apache.metamodel.util.SimpleTableDef; +import org.apache.metamodel.util.SimpleTableDefParser; + +public class DataContextSupplier implements Supplier<DataContext> { + + private final String dataSourceName; + private final DataSourceDefinition dataSourceDefinition; + + public DataContextSupplier(String dataSourceName, DataSourceDefinition dataSourceDefinition) { + this.dataSourceName = dataSourceName; + this.dataSourceDefinition = dataSourceDefinition; + } + + @Override + public DataContext get() { + final String type = dataSourceDefinition.getType(); + switch (type.toLowerCase()) { + case "pojo": + final List<TableDataProvider<?>> tableDataProviders; + + final Object tableDefinitions = dataSourceDefinition.getTableDefinitions(); + if (tableDefinitions == null) { + tableDataProviders = new ArrayList<>(0); + } else if (tableDefinitions instanceof String) { + final SimpleTableDef[] tableDefs = SimpleTableDefParser.parseTableDefs((String) tableDefinitions); + tableDataProviders = Arrays.stream(tableDefs).map((tableDef) -> { + return new ArrayTableDataProvider(tableDef, new ArrayList<Object[]>()); + }).collect(Collectors.toList()); + } else { + throw new UnsupportedOperationException("Unsupported table definition type: " + tableDefinitions); + } + + final String schemaName = dataSourceDefinition.getSchemaName() == null ? dataSourceName + : dataSourceDefinition.getSchemaName(); + + return new PojoDataContext(schemaName, tableDataProviders); + default: + throw new UnsupportedOperationException("Unsupported data source type: " + type); + } + } + +} http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/main/java/org/apache/metamodel/service/app/DataSourceDefinition.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/main/java/org/apache/metamodel/service/app/DataSourceDefinition.java b/service-webapp/src/main/java/org/apache/metamodel/service/app/DataSourceDefinition.java index 943222a..11140a7 100644 --- a/service-webapp/src/main/java/org/apache/metamodel/service/app/DataSourceDefinition.java +++ b/service-webapp/src/main/java/org/apache/metamodel/service/app/DataSourceDefinition.java @@ -20,5 +20,11 @@ package org.apache.metamodel.service.app; public interface DataSourceDefinition { + public String getType(); + + public Object getTableDefinitions(); + + public String getSchemaName(); + // TODO } http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/main/java/org/apache/metamodel/service/app/InMemoryDataSourceRegistry.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/main/java/org/apache/metamodel/service/app/InMemoryDataSourceRegistry.java b/service-webapp/src/main/java/org/apache/metamodel/service/app/InMemoryDataSourceRegistry.java index f3c45e8..3a6ecee 100644 --- a/service-webapp/src/main/java/org/apache/metamodel/service/app/InMemoryDataSourceRegistry.java +++ b/service-webapp/src/main/java/org/apache/metamodel/service/app/InMemoryDataSourceRegistry.java @@ -25,7 +25,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.metamodel.DataContext; -import org.apache.metamodel.pojo.PojoDataContext; public class InMemoryDataSourceRegistry implements DataSourceRegistry { @@ -36,19 +35,14 @@ public class InMemoryDataSourceRegistry implements DataSourceRegistry { } @Override - public String registerDataSource(final String dataContextIdentifier, final DataSourceDefinition dataContextDef) + public String registerDataSource(final String name, final DataSourceDefinition dataSourceDef) throws IllegalArgumentException { - if (dataSources.containsKey(dataContextIdentifier)) { - throw new IllegalArgumentException("DataContext already exist: " + dataContextIdentifier); + if (dataSources.containsKey(name)) { + throw new IllegalArgumentException("DataContext already exist: " + name); } - dataSources.put(dataContextIdentifier, new Supplier<DataContext>() { - @Override - public DataContext get() { - // TODO: Do a proper transformation from definition to instance - return new PojoDataContext(); - } - }); - return dataContextIdentifier; + + dataSources.put(name, new DataContextSupplier(name, dataSourceDef)); + return name; } @Override @@ -57,10 +51,10 @@ public class InMemoryDataSourceRegistry implements DataSourceRegistry { } @Override - public DataContext openDataContext(String dataContextIdentifier) { - final Supplier<DataContext> supplier = dataSources.get(dataContextIdentifier); + public DataContext openDataContext(String name) { + final Supplier<DataContext> supplier = dataSources.get(name); if (supplier == null) { - throw new IllegalArgumentException("No such DataContext: " + dataContextIdentifier); + throw new IllegalArgumentException("No such DataContext: " + name); } return supplier.get(); } http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/main/java/org/apache/metamodel/service/controllers/QueryController.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/main/java/org/apache/metamodel/service/controllers/QueryController.java b/service-webapp/src/main/java/org/apache/metamodel/service/controllers/QueryController.java new file mode 100644 index 0000000..e5c1e72 --- /dev/null +++ b/service-webapp/src/main/java/org/apache/metamodel/service/controllers/QueryController.java @@ -0,0 +1,79 @@ +/** + * 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.service.controllers; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.metamodel.DataContext; +import org.apache.metamodel.data.DataSet; +import org.apache.metamodel.query.Query; +import org.apache.metamodel.service.app.TenantContext; +import org.apache.metamodel.service.app.TenantRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +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; + +@RestController +@RequestMapping(value = { "/{tenant}/{dataContext}/query", + "/{tenant}/{dataContext}/q" }, produces = MediaType.APPLICATION_JSON_VALUE) +public class QueryController { + + private final TenantRegistry tenantRegistry; + + @Autowired + public QueryController(TenantRegistry tenantRegistry) { + this.tenantRegistry = tenantRegistry; + } + + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public Map<String, Object> get(@PathVariable("tenant") String tenantId, + @PathVariable("dataContext") String dataSourceName, + @RequestParam(value = "sql", required = true) String queryString, + @RequestParam(value = "offset", required = false) Integer offset, + @RequestParam(value = "limit", required = false) Integer limit) { + final TenantContext tenantContext = tenantRegistry.getTenantContext(tenantId); + final DataContext dataContext = tenantContext.getDataSourceRegistry().openDataContext(dataSourceName); + + final Query query = dataContext.parseQuery(queryString); + if (offset != null) { + query.setFirstRow(offset); + } + if (limit != null) { + query.setMaxRows(limit); + } + + final DataSet dataSet = dataContext.executeQuery(query); + + final Map<String, Object> map = new LinkedHashMap<>(); + map.put("type", "dataset"); + map.put("header", Arrays.stream(dataSet.getSelectItems()).map((si) -> si.toString()).collect(Collectors + .toList())); + map.put("data", dataSet.toObjectArrays()); + return map; + } +} http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/main/java/org/apache/metamodel/service/controllers/model/RestDataSourceDefinition.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/main/java/org/apache/metamodel/service/controllers/model/RestDataSourceDefinition.java b/service-webapp/src/main/java/org/apache/metamodel/service/controllers/model/RestDataSourceDefinition.java index ad5f5ba..48c5926 100644 --- a/service-webapp/src/main/java/org/apache/metamodel/service/controllers/model/RestDataSourceDefinition.java +++ b/service-webapp/src/main/java/org/apache/metamodel/service/controllers/model/RestDataSourceDefinition.java @@ -22,12 +22,35 @@ import javax.validation.constraints.NotNull; import org.apache.metamodel.service.app.DataSourceDefinition; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; public class RestDataSourceDefinition implements DataSourceDefinition { @JsonProperty(value = "type", required = true) @NotNull - public String type; + private String type; + @JsonProperty(value = "table-definitions", required = false) + @JsonInclude(JsonInclude.Include.NON_NULL) + private Object tableDefinitions; + + @JsonProperty(value = "schema-name", required = false) + @JsonInclude(JsonInclude.Include.NON_NULL) + private String schemaName; + + @Override + public String getType() { + return type; + } + + @Override + public Object getTableDefinitions() { + return tableDefinitions; + } + + @Override + public String getSchemaName() { + return schemaName; + } } http://git-wip-us.apache.org/repos/asf/metamodel/blob/8d4d0351/service-webapp/src/test/java/org/apache/metamodel/service/controllers/TenantInteractionScenarioTest.java ---------------------------------------------------------------------- diff --git a/service-webapp/src/test/java/org/apache/metamodel/service/controllers/TenantInteractionScenarioTest.java b/service-webapp/src/test/java/org/apache/metamodel/service/controllers/TenantInteractionScenarioTest.java index acdbb15..35c4695 100644 --- a/service-webapp/src/test/java/org/apache/metamodel/service/controllers/TenantInteractionScenarioTest.java +++ b/service-webapp/src/test/java/org/apache/metamodel/service/controllers/TenantInteractionScenarioTest.java @@ -48,9 +48,10 @@ public class TenantInteractionScenarioTest { SchemaController schemaController = new SchemaController(tenantRegistry); TableController tableController = new TableController(tenantRegistry); ColumnController columnController = new ColumnController(tenantRegistry); + QueryController queryController = new QueryController(tenantRegistry); mockMvc = MockMvcBuilders.standaloneSetup(tenantController, dataContextController, schemaController, - tableController, columnController).build(); + tableController, columnController, queryController).build(); } @Test @@ -70,15 +71,16 @@ public class TenantInteractionScenarioTest { // create datasource { final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.put("/tenant1/mydata").content( - "{'type':'pojo'}".replace('\'', '"')).contentType(MediaType.APPLICATION_JSON)).andExpect( - MockMvcResultMatchers.status().isOk()).andReturn(); + "{'type':'pojo','table-definitions':'hello world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);'}" + .replace('\'', '"')).contentType(MediaType.APPLICATION_JSON)).andExpect( + MockMvcResultMatchers.status().isOk()).andReturn(); final String content = result.getResponse().getContentAsString(); final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class); assertEquals("datasource", map.get("type")); assertEquals("mydata", map.get("name")); assertEquals( - "[{name=information_schema, uri=/tenant1/mydata/s/information_schema}, {name=Schema, uri=/tenant1/mydata/s/Schema}]", + "[{name=information_schema, uri=/tenant1/mydata/s/information_schema}, {name=mydata, uri=/tenant1/mydata/s/mydata}]", map.get("schemas").toString()); } @@ -96,56 +98,60 @@ public class TenantInteractionScenarioTest { // explore schema { - final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/s/information_schema")) - .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); + final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/s/mydata")).andExpect( + MockMvcResultMatchers.status().isOk()).andReturn(); final String content = result.getResponse().getContentAsString(); final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class); assertEquals("schema", map.get("type")); - assertEquals("information_schema", map.get("name")); + assertEquals("mydata", map.get("name")); - assertEquals("[{name=tables, uri=/tenant1/mydata/s/information_schema/t/tables}, " - + "{name=columns, uri=/tenant1/mydata/s/information_schema/t/columns}, " - + "{name=relationships, uri=/tenant1/mydata/s/information_schema/t/relationships}]", map.get( - "tables").toString()); + assertEquals( + "[{name=foo, uri=/tenant1/mydata/s/mydata/t/foo}, {name=hello world, uri=/tenant1/mydata/s/mydata/t/hello%20world}]", + map.get("tables").toString()); } // explore table { - final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get( - "/tenant1/mydata/s/information_schema/t/columns")).andExpect(MockMvcResultMatchers.status().isOk()) - .andReturn(); + final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/s/mydata/t/foo")) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); final String content = result.getResponse().getContentAsString(); final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class); assertEquals("table", map.get("type")); - assertEquals("columns", map.get("name")); - - assertEquals("[{name=name, uri=/tenant1/mydata/s/information_schema/t/columns/c/name}, " - + "{name=type, uri=/tenant1/mydata/s/information_schema/t/columns/c/type}, " - + "{name=native_type, uri=/tenant1/mydata/s/information_schema/t/columns/c/native_type}, " - + "{name=size, uri=/tenant1/mydata/s/information_schema/t/columns/c/size}, " - + "{name=nullable, uri=/tenant1/mydata/s/information_schema/t/columns/c/nullable}, " - + "{name=indexed, uri=/tenant1/mydata/s/information_schema/t/columns/c/indexed}, " - + "{name=table, uri=/tenant1/mydata/s/information_schema/t/columns/c/table}, " - + "{name=remarks, uri=/tenant1/mydata/s/information_schema/t/columns/c/remarks}]", map.get( - "columns").toString()); + assertEquals("foo", map.get("name")); + + assertEquals("[{name=bar, uri=/tenant1/mydata/s/mydata/t/foo/c/bar}, " + + "{name=baz, uri=/tenant1/mydata/s/mydata/t/foo/c/baz}]", map.get("columns").toString()); } // explore column { - final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get( - "/tenant1/mydata/s/information_schema/t/columns/c/nullable")).andExpect(MockMvcResultMatchers - .status().isOk()).andReturn(); + final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/s/mydata/t/foo/c/bar")) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); final String content = result.getResponse().getContentAsString(); final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class); assertEquals("column", map.get("type")); - assertEquals("nullable", map.get("name")); + assertEquals("bar", map.get("name")); assertEquals( - "{number=4, size=null, nullable=true, primary-key=false, indexed=false, column-type=BOOLEAN, native-type=null, remarks=null}", + "{number=0, size=null, nullable=true, primary-key=false, indexed=false, column-type=INTEGER, native-type=null, remarks=null}", map.get("metadata").toString()); } + + // query metadata from information_schema + { + final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/tenant1/mydata/q?sql={sql}", + "SELECT name, table FROM information_schema.columns")).andExpect(MockMvcResultMatchers.status() + .isOk()).andReturn(); + + final String content = result.getResponse().getContentAsString(); + final Map<?, ?> map = new ObjectMapper().readValue(content, Map.class); + assertEquals("dataset", map.get("type")); + assertEquals("[columns.name, columns.table]", map.get("header").toString()); + assertEquals("[[bar, foo], [baz, foo], [greeting, hello world], [who, hello world]]", map.get("data") + .toString()); + } } }
