geode-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jche...@apache.org
Subject [geode] branch develop updated: GEODE-6025: add describe data-source (#2864)
Date Fri, 16 Nov 2018 18:53:31 GMT
This is an automated email from the ASF dual-hosted git repository.

jchen21 pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 0c1f584  GEODE-6025: add describe data-source (#2864)
0c1f584 is described below

commit 0c1f5849c7961c6f75d329bc05d5aa5c56872e79
Author: Darrel Schneider <dschneider@pivotal.io>
AuthorDate: Fri Nov 16 10:53:22 2018 -0800

    GEODE-6025: add describe data-source (#2864)
    
    In addition to adding the `describe data-source` gfsh command, the `create data-source`
gfsh command and related tests have been moved to `geode-connectors` from `geode-core`
    
    Co-authored-by: Darrel Schneider <dschneider@pivotal.io>
    Co-authored-by: Jianxia Chen <jchen@pivotal.io>
    Co-authored-by: Scott Jewell <sjewell@pivotal.io>
---
 geode-connectors/build.gradle                      |   1 +
 .../cli}/CreateDataSourceCommandDUnitTest.java     |   2 +-
 .../cli/DescribeDataSourceCommandDUnitTest.java    | 114 ++++++
 .../internal/cli}/CreateDataSourceCommand.java     |   4 +-
 .../internal/cli}/CreateDataSourceInterceptor.java |   3 +-
 .../internal/cli/DescribeDataSourceCommand.java    | 136 ++++++++
 .../cli/converters/PoolPropertyConverter.java      |  10 +-
 .../org.springframework.shell.core.CommandMarker   |   2 +
 .../internal/cli}/CreateDataSourceCommandTest.java |  11 +-
 .../cli}/CreateDataSourceInterceptorTest.java      |   2 +-
 .../cli/DescribeDataSourceCommandTest.java         | 382 +++++++++++++++++++++
 .../cli/commands/CommandAvailabilityIndicator.java |   1 -
 .../cli/commands/UsernamePasswordInterceptor.java  |   2 +-
 13 files changed, 652 insertions(+), 18 deletions(-)

diff --git a/geode-connectors/build.gradle b/geode-connectors/build.gradle
index 71b3e44..4ad624b 100644
--- a/geode-connectors/build.gradle
+++ b/geode-connectors/build.gradle
@@ -75,6 +75,7 @@ dependencies {
   distributedTestCompile('junit:junit:' + project.'junit.version')
   distributedTestCompile('org.assertj:assertj-core:' + project.'assertj-core.version')
   distributedTestCompile('org.mockito:mockito-core:2.19.1')
+  distributedTestRuntime('org.apache.derby:derby:' + project.'derby.version')
 
   acceptanceTestCompile('com.github.stefanbirkner:system-rules:' + project.'system-rules.version')
{
     exclude module: 'junit-dep'
diff --git a/geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
b/geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandDUnitTest.java
similarity index 98%
rename from geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
rename to geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandDUnitTest.java
index 8dc4843..dd4169c 100644
--- a/geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
+++ b/geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandDUnitTest.java
@@ -13,7 +13,7 @@
  * the License.
  */
 
-package org.apache.geode.management.internal.cli.commands;
+package org.apache.geode.connectors.jdbc.internal.cli;
 
 import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
 
diff --git a/geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandDUnitTest.java
b/geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandDUnitTest.java
new file mode 100644
index 0000000..dc51f64
--- /dev/null
+++ b/geode-connectors/src/distributedTest/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandDUnitTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.geode.connectors.jdbc.internal.cli;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class DescribeDataSourceCommandDUnitTest {
+
+  private MemberVM locator, server;
+
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  @Before
+  public void before() throws Exception {
+    locator = cluster.startLocatorVM(0);
+    server = cluster.startServerVM(1, new Properties(), locator.getPort());
+
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void describeDataSourceForSimpleDataSource() {
+    gfsh.executeAndAssertThat(
+        "create data-source --name=simple --url=\"jdbc:derby:newDB;create=true\" --username=joe
--password=myPassword")
+        .statusIsSuccess().tableHasColumnOnlyWithValues("Member", "server-1");
+
+    CommandResultAssert result = gfsh.executeAndAssertThat("describe data-source --name=simple");
+
+    result.statusIsSuccess()
+        .tableHasRowWithValues("Property", "Value", "name", "simple")
+        .tableHasRowWithValues("Property", "Value", "pooled", "false")
+        .tableHasRowWithValues("Property", "Value", "username", "joe")
+        .tableHasRowWithValues("Property", "Value", "url", "jdbc:derby:newDB;create=true");
+    assertThat(result.getResultModel().toString()).doesNotContain("myPassword");
+  }
+
+  @Test
+  public void describeDataSourceUsedByRegionsListsTheRegionsInOutput() {
+    gfsh.executeAndAssertThat(
+        "create data-source --name=simple --url=\"jdbc:derby:newDB;create=true\"")
+        .statusIsSuccess().tableHasColumnOnlyWithValues("Member", "server-1");
+    gfsh.executeAndAssertThat("create region --name=region1 --type=REPLICATE").statusIsSuccess();
+    gfsh.executeAndAssertThat("create region --name=region2 --type=PARTITION").statusIsSuccess();
+    gfsh.executeAndAssertThat(
+        "create jdbc-mapping --region=region1 --data-source=simple --pdx-name=myPdx");
+    gfsh.executeAndAssertThat(
+        "create jdbc-mapping --region=region2 --data-source=simple --pdx-name=myPdx");
+
+    CommandResultAssert result = gfsh.executeAndAssertThat("describe data-source --name=simple");
+
+    result.statusIsSuccess()
+        .tableHasRowWithValues("Property", "Value", "name", "simple")
+        .tableHasRowWithValues("Property", "Value", "pooled", "false")
+        .tableHasRowWithValues("Property", "Value", "url", "jdbc:derby:newDB;create=true");
+    InfoResultModel infoSection = result.getResultModel()
+        .getInfoSection(DescribeDataSourceCommand.REGIONS_USING_DATA_SOURCE_SECTION);
+    assertThat(new HashSet<>(infoSection.getContent()))
+        .isEqualTo(new HashSet<>(Arrays.asList("region1", "region2")));
+  }
+
+  @Test
+  public void describeDataSourceForPooledDataSource() {
+    gfsh.executeAndAssertThat(
+        "create data-source --name=pooled --pooled --url=\"jdbc:derby:newDB;create=true\"
--pooled-data-source-factory-class=org.apache.geode.internal.jta.CacheJTAPooledDataSourceFactory
--pool-properties={'name':'prop1','value':'value1'},{'name':'pool.prop2','value':'value2'}")
+        .statusIsSuccess().tableHasColumnOnlyWithValues("Member", "server-1");
+
+    gfsh.executeAndAssertThat("describe data-source --name=pooled").statusIsSuccess()
+        .tableHasRowWithValues("Property", "Value", "name", "pooled")
+        .tableHasRowWithValues("Property", "Value", "pooled", "true")
+        .tableHasRowWithValues("Property", "Value", "username", "")
+        .tableHasRowWithValues("Property", "Value", "url", "jdbc:derby:newDB;create=true")
+        .tableHasRowWithValues("Property", "Value", "pooled-data-source-factory-class",
+            "org.apache.geode.internal.jta.CacheJTAPooledDataSourceFactory")
+        .tableHasRowWithValues("Property", "Value", "prop1", "value1")
+        .tableHasRowWithValues("Property", "Value", "pool.prop2", "value2");
+  }
+
+  @Test
+  public void describeDataSourceDoesNotExist() {
+    gfsh.executeAndAssertThat("describe data-source --name=unknown").statusIsError()
+        .containsOutput("Data source: unknown not found");
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommand.java
similarity index 98%
rename from geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
rename to geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommand.java
index 7847700..345d182 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
+++ b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommand.java
@@ -12,7 +12,7 @@
  * or implied. See the License for the specific language governing permissions and limitations
under
  * the License.
  */
-package org.apache.geode.management.internal.cli.commands;
+package org.apache.geode.connectors.jdbc.internal.cli;
 
 import java.util.List;
 import java.util.Set;
@@ -74,7 +74,7 @@ public class CreateDataSourceCommand extends SingleGfshCommand {
 
   @CliCommand(value = CREATE_DATA_SOURCE, help = CREATE_DATA_SOURCE__HELP)
   @CliMetaData(relatedTopic = CliStrings.DEFAULT_TOPIC_GEODE,
-      interceptor = "org.apache.geode.management.internal.cli.commands.CreateDataSourceInterceptor")
+      interceptor = "org.apache.geode.connectors.jdbc.internal.cli.CreateDataSourceInterceptor")
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.MANAGE)
   public ResultModel createJDNIBinding(
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptor.java
b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptor.java
similarity index 94%
rename from geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptor.java
rename to geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptor.java
index 58343db..1c80ea6 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptor.java
+++ b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptor.java
@@ -14,9 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.geode.management.internal.cli.commands;
+package org.apache.geode.connectors.jdbc.internal.cli;
 
 import org.apache.geode.management.internal.cli.GfshParseResult;
+import org.apache.geode.management.internal.cli.commands.UsernamePasswordInterceptor;
 import org.apache.geode.management.internal.cli.result.model.ResultModel;
 import org.apache.geode.management.internal.cli.shell.Gfsh;
 
diff --git a/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommand.java
b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommand.java
new file mode 100644
index 0000000..611fe74
--- /dev/null
+++ b/geode-connectors/src/main/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommand.java
@@ -0,0 +1,136 @@
+/*
+ * 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.geode.connectors.jdbc.internal.cli;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.annotations.Experimental;
+import org.apache.geode.cache.configuration.CacheConfig;
+import org.apache.geode.cache.configuration.CacheElement;
+import org.apache.geode.cache.configuration.JndiBindingsType;
+import org.apache.geode.cache.configuration.RegionConfig;
+import org.apache.geode.connectors.jdbc.internal.configuration.RegionMapping;
+import org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.internal.cli.commands.CreateJndiBindingCommand.DATASOURCE_TYPE;
+import org.apache.geode.management.internal.cli.commands.InternalGfshCommand;
+import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+@Experimental
+public class DescribeDataSourceCommand extends InternalGfshCommand {
+  static final String DESCRIBE_DATA_SOURCE = "describe data-source";
+  private static final String DESCRIBE_DATA_SOURCE__HELP = EXPERIMENTAL +
+      "Describe the configuration of the given data source.";
+
+  static final String DATA_SOURCE_PROPERTIES_SECTION = "data-source-properties";
+  static final String REGIONS_USING_DATA_SOURCE_SECTION = "regions-using-data-source";
+
+  @CliCommand(value = DESCRIBE_DATA_SOURCE, help = DESCRIBE_DATA_SOURCE__HELP)
+  @CliMetaData
+  @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
+      operation = ResourcePermission.Operation.READ)
+  public ResultModel describeDataSource(@CliOption(key = "name", mandatory = true,
+      help = "Name of the data source to describe") String dataSourceName) {
+
+    ResultModel resultModel = new ResultModel();
+    resultModel.setHeader(EXPERIMENTAL);
+    TabularResultModel tabularData = resultModel.addTable(DATA_SOURCE_PROPERTIES_SECTION);
+
+    InternalConfigurationPersistenceService ccService = getConfigurationPersistenceService();
+    if (ccService == null) {
+      return ResultModel.createError("Cluster configuration service must be enabled.");
+    }
+    CacheConfig cacheConfig = ccService.getCacheConfig(null);
+    if (cacheConfig == null) {
+      return ResultModel.createError(String.format("Data source: %s not found", dataSourceName));
+    }
+
+    List<JndiBindingsType.JndiBinding> jndiBindings = cacheConfig.getJndiBindings();
+    JndiBindingsType.JndiBinding binding = jndiBindings.stream()
+        .filter(b -> b.getJndiName().equals(dataSourceName)).findFirst().orElse(null);
+    if (binding == null) {
+      return ResultModel.createError(String.format("Data source: %s not found", dataSourceName));
+    }
+    boolean pooled;
+    String type = binding.getType();
+    if (DATASOURCE_TYPE.SIMPLE.getType().equals(type)) {
+      pooled = false;
+    } else if (DATASOURCE_TYPE.POOLED.getType().equals(type)) {
+      pooled = true;
+    } else {
+      return ResultModel.createError(String.format("Unknown data source type: %s", type));
+    }
+
+    addTableRow(tabularData, CreateDataSourceCommand.NAME, binding.getJndiName());
+    addTableRow(tabularData, CreateDataSourceCommand.URL, binding.getConnectionUrl());
+    addTableRow(tabularData, CreateDataSourceCommand.USERNAME, binding.getUserName());
+    addTableRow(tabularData, CreateDataSourceCommand.POOLED, Boolean.toString(pooled));
+    if (pooled) {
+      addTableRow(tabularData, CreateDataSourceCommand.POOLED_DATA_SOURCE_FACTORY_CLASS,
+          binding.getConnPooledDatasourceClass());
+      for (JndiBindingsType.JndiBinding.ConfigProperty confProp : binding.getConfigProperties())
{
+        addTableRow(tabularData, confProp.getName(), confProp.getValue());
+      }
+    }
+
+    InfoResultModel regionsUsingSection = resultModel.addInfo(REGIONS_USING_DATA_SOURCE_SECTION);
+    List<String> regionsUsing = getRegionsThatUseDataSource(cacheConfig, dataSourceName);
+    regionsUsingSection.setHeader("Regions Using Data Source:");
+    if (regionsUsing.isEmpty()) {
+      regionsUsingSection.addLine("no regions are using " + dataSourceName);
+    } else {
+      regionsUsingSection.setContent(regionsUsing);
+    }
+
+    return resultModel;
+  }
+
+  List<String> getRegionsThatUseDataSource(CacheConfig cacheConfig, String dataSourceName)
{
+    return cacheConfig.getRegions()
+        .stream()
+        .filter(regionConfig -> hasJdbcMappingThatUsesDataSource(regionConfig, dataSourceName))
+        .map(RegionConfig::getName)
+        .collect(Collectors.toList());
+  }
+
+  private boolean hasJdbcMappingThatUsesDataSource(RegionConfig regionConfig,
+      String dataSourceName) {
+    return regionConfig.getCustomRegionElements()
+        .stream()
+        .anyMatch(cacheElement -> isRegionMappingUsingDataSource(cacheElement, dataSourceName));
+  }
+
+  private boolean isRegionMappingUsingDataSource(CacheElement cacheElement, String dataSourceName)
{
+    if (!(cacheElement instanceof RegionMapping)) {
+      return false;
+    }
+    RegionMapping regionMapping = (RegionMapping) cacheElement;
+    return dataSourceName.equals(regionMapping.getDataSourceName());
+  }
+
+  private void addTableRow(TabularResultModel table, String property, String value) {
+    table.accumulate("Property", property);
+    table.accumulate("Value", value != null ? value : "");
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
b/geode-connectors/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
similarity index 83%
rename from geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
rename to geode-connectors/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
index 374c6f9..f5b7cd7 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
+++ b/geode-connectors/src/main/java/org/apache/geode/management/internal/cli/converters/PoolPropertyConverter.java
@@ -23,14 +23,14 @@ import org.springframework.shell.core.Completion;
 import org.springframework.shell.core.Converter;
 import org.springframework.shell.core.MethodTarget;
 
-import org.apache.geode.management.internal.cli.commands.CreateDataSourceCommand.PoolProperty;
+import org.apache.geode.connectors.jdbc.internal.cli.CreateDataSourceCommand;
 
 /***
  * Converter for CreateDataSourceCommand's --pool-properties option.
  *
  */
 public class PoolPropertyConverter
-    implements Converter<PoolProperty> {
+    implements Converter<CreateDataSourceCommand.PoolProperty> {
 
   private static ObjectMapper mapper = new ObjectMapper();
   static {
@@ -39,14 +39,14 @@ public class PoolPropertyConverter
 
   @Override
   public boolean supports(Class<?> type, String optionContext) {
-    return PoolProperty.class.isAssignableFrom(type);
+    return CreateDataSourceCommand.PoolProperty.class.isAssignableFrom(type);
   }
 
   @Override
-  public PoolProperty convertFromText(String value,
+  public CreateDataSourceCommand.PoolProperty convertFromText(String value,
       Class<?> targetType, String optionContext) {
     try {
-      return mapper.readValue(value, PoolProperty.class);
+      return mapper.readValue(value, CreateDataSourceCommand.PoolProperty.class);
     } catch (IOException e) {
       throw new IllegalArgumentException("invalid json: \"" + value + "\" details: " + e);
     }
diff --git a/geode-connectors/src/main/resources/META-INF/services/org.springframework.shell.core.CommandMarker
b/geode-connectors/src/main/resources/META-INF/services/org.springframework.shell.core.CommandMarker
index bf3e1eb..a6ae8aa 100644
--- a/geode-connectors/src/main/resources/META-INF/services/org.springframework.shell.core.CommandMarker
+++ b/geode-connectors/src/main/resources/META-INF/services/org.springframework.shell.core.CommandMarker
@@ -15,7 +15,9 @@
 # limitations under the License.
 #
 # JDBC Connector Extension commands
+org.apache.geode.connectors.jdbc.internal.cli.CreateDataSourceCommand
 org.apache.geode.connectors.jdbc.internal.cli.CreateMappingCommand
+org.apache.geode.connectors.jdbc.internal.cli.DescribeDataSourceCommand
 org.apache.geode.connectors.jdbc.internal.cli.DestroyMappingCommand
 org.apache.geode.connectors.jdbc.internal.cli.DescribeMappingCommand
 org.apache.geode.connectors.jdbc.internal.cli.ListMappingCommand
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandTest.java
b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandTest.java
similarity index 97%
rename from geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandTest.java
rename to geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandTest.java
index ea4e8d7..6f5f340 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandTest.java
+++ b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceCommandTest.java
@@ -12,7 +12,7 @@
  * or implied. See the License for the specific language governing permissions and limitations
under
  * the License.
  */
-package org.apache.geode.management.internal.cli.commands;
+package org.apache.geode.connectors.jdbc.internal.cli;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -39,6 +38,7 @@ import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 import org.xml.sax.SAXException;
 
 import org.apache.geode.cache.configuration.CacheConfig;
@@ -49,7 +49,6 @@ import org.apache.geode.distributed.internal.DistributionManager;
 import org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
 import org.apache.geode.internal.cache.InternalCache;
 import org.apache.geode.management.internal.cli.GfshParseResult;
-import org.apache.geode.management.internal.cli.commands.CreateDataSourceCommand.PoolProperty;
 import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
 import org.apache.geode.management.internal.cli.functions.CreateJndiBindingFunction;
 import org.apache.geode.test.junit.rules.GfshParserRule;
@@ -70,7 +69,7 @@ public class CreateDataSourceCommandTest {
   public void setUp() throws Exception {
     cache = mock(InternalCache.class);
     when(cache.getDistributionManager()).thenReturn(mock(DistributionManager.class));
-    command = spy(CreateDataSourceCommand.class);
+    command = Mockito.spy(CreateDataSourceCommand.class);
     command.setCache(cache);
 
     binding = new JndiBindingsType.JndiBinding();
@@ -101,8 +100,8 @@ public class CreateDataSourceCommandTest {
         + " --pooled --name=name --url=url "
         + "--pool-properties={'name':'name1','value':'value1'},{'name':'name2','value':'value2'}");
 
-    PoolProperty[] poolProperties =
-        (PoolProperty[]) result
+    CreateDataSourceCommand.PoolProperty[] poolProperties =
+        (CreateDataSourceCommand.PoolProperty[]) result
             .getParamValue("pool-properties");
     assertThat(poolProperties).hasSize(2);
     assertThat(poolProperties[0].getName()).isEqualTo("name1");
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptorTest.java
b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptorTest.java
similarity index 98%
rename from geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptorTest.java
rename to geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptorTest.java
index 83c40b7..145d021 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceInterceptorTest.java
+++ b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/CreateDataSourceInterceptorTest.java
@@ -12,7 +12,7 @@
  * or implied. See the License for the specific language governing permissions and limitations
under
  * the License.
  */
-package org.apache.geode.management.internal.cli.commands;
+package org.apache.geode.connectors.jdbc.internal.cli;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
diff --git a/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandTest.java
b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandTest.java
new file mode 100644
index 0000000..abba7a2
--- /dev/null
+++ b/geode-connectors/src/test/java/org/apache/geode/connectors/jdbc/internal/cli/DescribeDataSourceCommandTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.geode.connectors.jdbc.internal.cli;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import org.apache.geode.cache.configuration.CacheConfig;
+import org.apache.geode.cache.configuration.CacheElement;
+import org.apache.geode.cache.configuration.JndiBindingsType;
+import org.apache.geode.cache.configuration.JndiBindingsType.JndiBinding.ConfigProperty;
+import org.apache.geode.cache.configuration.RegionConfig;
+import org.apache.geode.connectors.jdbc.internal.configuration.RegionMapping;
+import org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
+import org.apache.geode.management.cli.Result.Status;
+import org.apache.geode.management.internal.cli.commands.CreateJndiBindingCommand.DATASOURCE_TYPE;
+import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
+import org.apache.geode.test.junit.rules.GfshParserRule;
+
+public class DescribeDataSourceCommandTest {
+
+  @ClassRule
+  public static GfshParserRule gfsh = new GfshParserRule();
+
+  private DescribeDataSourceCommand command;
+
+  private JndiBindingsType.JndiBinding binding;
+  private List<JndiBindingsType.JndiBinding> bindings;
+  private InternalConfigurationPersistenceService clusterConfigService;
+  private CacheConfig cacheConfig;
+  private List<RegionConfig> regionConfigs;
+
+  private static String COMMAND = "describe data-source";
+  private static String DATA_SOURCE_NAME = "myDataSource";
+
+  @Before
+  public void setUp() {
+    command = spy(DescribeDataSourceCommand.class);
+
+    binding = new JndiBindingsType.JndiBinding();
+    binding.setJndiName(DATA_SOURCE_NAME);
+    binding.setType(DATASOURCE_TYPE.POOLED.getType());
+    bindings = new ArrayList<>();
+    clusterConfigService = mock(InternalConfigurationPersistenceService.class);
+    cacheConfig = mock(CacheConfig.class);
+    when(cacheConfig.getJndiBindings()).thenReturn(bindings);
+    bindings.add(binding);
+    regionConfigs = new ArrayList<>();
+    when(cacheConfig.getRegions()).thenReturn(regionConfigs);
+
+    doReturn(clusterConfigService).when(command).getConfigurationPersistenceService();
+    doReturn(cacheConfig).when(clusterConfigService).getCacheConfig(any());
+  }
+
+  @Test
+  public void missingMandatory() {
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsError()
+        .containsOutput("Invalid command: describe data-source");
+  }
+
+  @Test
+  public void nameWorks() {
+    gfsh.executeAndAssertThat(command, COMMAND + " --name=" + DATA_SOURCE_NAME).statusIsSuccess();
+  }
+
+  @Test
+  public void describeDataSourceWithNoClusterConfigurationServerFails() {
+    doReturn(null).when(command).getConfigurationPersistenceService();
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    assertThat(result.getStatus()).isEqualTo(Status.ERROR);
+    assertThat(result.toString()).contains("Cluster configuration service must be enabled.");
+  }
+
+  @Test
+  public void describeDataSourceWithNoClusterConfigFails() {
+    doReturn(null).when(clusterConfigService).getCacheConfig(any());
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    assertThat(result.getStatus()).isEqualTo(Status.ERROR);
+    assertThat(result.toString()).contains("Data source: " + DATA_SOURCE_NAME + " not found");
+  }
+
+  @Test
+  public void describeDataSourceWithWrongNameFails() {
+    ResultModel result = command.describeDataSource("bogusName");
+
+    assertThat(result.getStatus()).isEqualTo(Status.ERROR);
+    assertThat(result.toString()).contains("Data source: bogusName not found");
+  }
+
+  @Test
+  public void describeDataSourceWithUnsupportedTypeFails() {
+    binding.setType(DATASOURCE_TYPE.MANAGED.getType());
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    assertThat(result.getStatus()).isEqualTo(Status.ERROR);
+    assertThat(result.toString()).contains("Unknown data source type: ManagedDataSource");
+  }
+
+  @Test
+  public void describeDataSourceWithSimpleTypeReturnsPooledFalse() {
+    binding.setType(DATASOURCE_TYPE.SIMPLE.getType());
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(3)).isEqualTo(Arrays.asList("pooled", "false"));
+  }
+
+  @Test
+  public void describeDataSourceWithPooledTypeReturnsPooledTrue() {
+    binding.setType(DATASOURCE_TYPE.POOLED.getType());
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(3)).isEqualTo(Arrays.asList("pooled", "true"));
+  }
+
+  @Test
+  public void describeDataSourceTypeReturnsName() {
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(0)).isEqualTo(Arrays.asList("name", DATA_SOURCE_NAME));
+  }
+
+  @Test
+  public void describeDataSourceWithUrlReturnsUrl() {
+    binding.setConnectionUrl("myUrl");
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(1)).isEqualTo(Arrays.asList("url", "myUrl"));
+  }
+
+  @Test
+  public void describeDataSourceWithUsernameReturnsUsername() {
+    binding.setUserName("myUserName");
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(2)).isEqualTo(Arrays.asList("username", "myUserName"));
+  }
+
+  @Test
+  public void describeDataSourceWithPooledDataSourceFactoryClassShowsItInTheResult() {
+    binding.setType(DATASOURCE_TYPE.POOLED.getType());
+    binding.setConnPooledDatasourceClass("myPooledDataSourceFactoryClass");
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(4)).isEqualTo(
+        Arrays.asList("pooled-data-source-factory-class", "myPooledDataSourceFactoryClass"));
+  }
+
+  @Test
+  public void describeDataSourceWithPasswordDoesNotShowPasswordInResult() {
+    binding.setType(DATASOURCE_TYPE.POOLED.getType());
+    binding.setPassword("myPassword");
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    assertThat(result.toString()).doesNotContain("myPassword");
+  }
+
+  @Test
+  public void describeDataSourceWithPoolPropertiesDoesNotShowsItInTheResult() {
+    binding.setType(DATASOURCE_TYPE.SIMPLE.getType());
+    List<ConfigProperty> configProperties = binding.getConfigProperties();
+    configProperties.add(new ConfigProperty("name1", "value1"));
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    assertThat(result.toString()).doesNotContain("name1");
+    assertThat(result.toString()).doesNotContain("value1");
+  }
+
+  @Test
+  public void describeDataSourceWithPoolPropertiesShowsItInTheResult() {
+    binding.setType(DATASOURCE_TYPE.POOLED.getType());
+    List<ConfigProperty> configProperties = binding.getConfigProperties();
+    configProperties.add(new ConfigProperty("name1", "value1"));
+    configProperties.add(new ConfigProperty("name2", "value2"));
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    TabularResultModel section =
+        result.getTableSection(DescribeDataSourceCommand.DATA_SOURCE_PROPERTIES_SECTION);
+    assertThat(section.getValuesInRow(5)).isEqualTo(Arrays.asList("name1", "value1"));
+    assertThat(section.getValuesInRow(6)).isEqualTo(Arrays.asList("name2", "value2"));
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenNoRegionsReturnsEmptyList() {
+    regionConfigs.clear();
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenRegionConfigWithNoCustomRegionElementsReturnsEmptyList()
{
+    RegionConfig regionConfig = mock(RegionConfig.class);
+    when(regionConfig.getCustomRegionElements()).thenReturn(Collections.emptyList());
+    regionConfigs.add(regionConfig);
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenRegionConfigWithNonRegionMappingElementReturnsEmptyList()
{
+    RegionConfig regionConfig = mock(RegionConfig.class);
+    when(regionConfig.getCustomRegionElements())
+        .thenReturn(Collections.singletonList(mock(CacheElement.class)));
+    regionConfigs.add(regionConfig);
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenRegionConfigWithRegionMappingForOtherDataSourceReturnsEmptyList()
{
+    RegionConfig regionConfig = mock(RegionConfig.class);
+    when(regionConfig.getCustomRegionElements())
+        .thenReturn(Collections.singletonList(mock(RegionMapping.class)));
+    regionConfigs.add(regionConfig);
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "bogusDataSource");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenRegionConfigWithRegionMappingForDataSourceReturnsRegionName()
{
+    RegionConfig regionConfig = mock(RegionConfig.class);
+    when(regionConfig.getName()).thenReturn("regionName");
+    RegionMapping regionMapping = mock(RegionMapping.class);
+    when(regionMapping.getDataSourceName()).thenReturn("dataSourceName");
+    when(regionConfig.getCustomRegionElements())
+        .thenReturn(Collections.singletonList(regionMapping));
+    regionConfigs.add(regionConfig);
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "dataSourceName");
+
+    assertThat(result).isEqualTo(Collections.singletonList("regionName"));
+  }
+
+  @Test
+  public void getRegionsThatUseDataSourceGivenMultipleRegionConfigsReturnsAllRegionNames()
{
+    RegionMapping regionMapping;
+    {
+      RegionConfig regionConfig1 = mock(RegionConfig.class, "regionConfig1");
+      when(regionConfig1.getName()).thenReturn("regionName1");
+      regionMapping = mock(RegionMapping.class, "regionMapping1");
+      when(regionMapping.getDataSourceName()).thenReturn("dataSourceName");
+      when(regionConfig1.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig1);
+    }
+    {
+      RegionConfig regionConfig2 = mock(RegionConfig.class, "regionConfig2");
+      when(regionConfig2.getName()).thenReturn("regionName2");
+      regionMapping = mock(RegionMapping.class, "regionMapping2");
+      when(regionMapping.getDataSourceName()).thenReturn("otherDataSourceName");
+      when(regionConfig2.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig2);
+    }
+    {
+      RegionConfig regionConfig3 = mock(RegionConfig.class, "regionConfig3");
+      when(regionConfig3.getName()).thenReturn("regionName3");
+      regionMapping = mock(RegionMapping.class, "regionMapping3");
+      when(regionMapping.getDataSourceName()).thenReturn("dataSourceName");
+      when(regionConfig3.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig3);
+    }
+
+    List<String> result = command.getRegionsThatUseDataSource(cacheConfig, "dataSourceName");
+
+    assertThat(result).isEqualTo(Arrays.asList("regionName1", "regionName3"));
+  }
+
+  @Test
+  public void describeDataSourceWithRegionsUsingItReturnsResultWithNoRegionsUsingIt() {
+    RegionConfig regionConfig = mock(RegionConfig.class);
+    when(regionConfig.getCustomRegionElements())
+        .thenReturn(Collections.singletonList(mock(RegionMapping.class)));
+    regionConfigs.add(regionConfig);
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    InfoResultModel regionsUsingSection = (InfoResultModel) result
+        .getSection(DescribeDataSourceCommand.REGIONS_USING_DATA_SOURCE_SECTION);
+    assertThat(regionsUsingSection.getContent())
+        .isEqualTo(Arrays.asList("no regions are using " + DATA_SOURCE_NAME));
+  }
+
+  @Test
+  public void describeDataSourceWithRegionsUsingItReturnsResultWithRegionNames() {
+    RegionMapping regionMapping;
+    {
+      RegionConfig regionConfig1 = mock(RegionConfig.class, "regionConfig1");
+      when(regionConfig1.getName()).thenReturn("regionName1");
+      regionMapping = mock(RegionMapping.class, "regionMapping1");
+      when(regionMapping.getDataSourceName()).thenReturn(DATA_SOURCE_NAME);
+      when(regionConfig1.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig1);
+    }
+    {
+      RegionConfig regionConfig2 = mock(RegionConfig.class, "regionConfig2");
+      when(regionConfig2.getName()).thenReturn("regionName2");
+      regionMapping = mock(RegionMapping.class, "regionMapping2");
+      when(regionMapping.getDataSourceName()).thenReturn("otherDataSourceName");
+      when(regionConfig2.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig2);
+    }
+    {
+      RegionConfig regionConfig3 = mock(RegionConfig.class, "regionConfig3");
+      when(regionConfig3.getName()).thenReturn("regionName3");
+      regionMapping = mock(RegionMapping.class, "regionMapping3");
+      when(regionMapping.getDataSourceName()).thenReturn(DATA_SOURCE_NAME);
+      when(regionConfig3.getCustomRegionElements())
+          .thenReturn(Arrays.asList(regionMapping));
+      regionConfigs.add(regionConfig3);
+    }
+
+    ResultModel result = command.describeDataSource(DATA_SOURCE_NAME);
+
+    InfoResultModel regionsUsingSection = (InfoResultModel) result
+        .getSection(DescribeDataSourceCommand.REGIONS_USING_DATA_SOURCE_SECTION);
+    assertThat(regionsUsingSection.getContent())
+        .isEqualTo(Arrays.asList("regionName1", "regionName3"));
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CommandAvailabilityIndicator.java
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CommandAvailabilityIndicator.java
index 516ede4..a2d6049 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CommandAvailabilityIndicator.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CommandAvailabilityIndicator.java
@@ -51,7 +51,6 @@ public class CommandAvailabilityIndicator extends GfshCommand {
       CliStrings.DESTROY_GATEWAYSENDER, AlterAsyncEventQueueCommand.COMMAND_NAME,
       DestroyAsyncEventQueueCommand.DESTROY_ASYNC_EVENT_QUEUE,
       DestroyGatewayReceiverCommand.DESTROY_GATEWAYRECEIVER,
-      CreateDataSourceCommand.CREATE_DATA_SOURCE,
       CreateJndiBindingCommand.CREATE_JNDIBINDING, DestroyJndiBindingCommand.DESTROY_JNDIBINDING,
       DescribeJndiBindingCommand.DESCRIBE_JNDI_BINDING, ListJndiBindingCommand.LIST_JNDIBINDING})
   public boolean available() {
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/UsernamePasswordInterceptor.java
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/UsernamePasswordInterceptor.java
index c3a5d1b..195ab85 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/UsernamePasswordInterceptor.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/UsernamePasswordInterceptor.java
@@ -30,7 +30,7 @@ public class UsernamePasswordInterceptor extends AbstractCliAroundInterceptor
{
   }
 
   // Constructor for unit test
-  UsernamePasswordInterceptor(Gfsh gfsh) {
+  public UsernamePasswordInterceptor(Gfsh gfsh) {
     this.gfsh = gfsh;
   }
 


Mime
View raw message