This is an automated email from the ASF dual-hosted git repository.
dschneider pushed a commit to branch feature/GEODE-5951
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/feature/GEODE-5951 by this
push:
new ccef399 wip: create data-source command added and it seems to work.
The most recent change worked on was the --properties option. It now parses
just name:value pairs (no more type) but a TODO exists for it on
CreateDataSourceCommand and that file currently does not compile. More tests
need to be added for this new command.
ccef399 is described below
commit ccef399aea8f2808fbe5c0b2e69a7b6d1b2d6535
Author: Darrel Schneider <[email protected]>
AuthorDate: Tue Oct 30 08:40:21 2018 -0700
wip: create data-source command added and it seems to work.
The most recent change worked on was the --properties option.
It now parses just name:value pairs (no more type) but a TODO
exists for it on CreateDataSourceCommand and that file currently
does not compile.
More tests need to be added for this new command.
---
.../commands/CreateDataSourceCommandDUnitTest.java | 125 +++++++++++++++
.../cache/configuration/JndiBindingsType.java | 17 ++
.../cli/commands/CreateDataSourceCommand.java | 176 +++++++++++++++++++++
.../converters/DataSourcePropertyConverter.java | 60 +++++++
.../cli/functions/CreateJndiBindingFunction.java | 14 +-
5 files changed, 388 insertions(+), 4 deletions(-)
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
new file mode 100644
index 0000000..0dcc960
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommandDUnitTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.management.internal.cli.commands;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import
org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
+import org.apache.geode.internal.jndi.JNDIInvoker;
+import org.apache.geode.internal.logging.LogService;
+import org.apache.geode.management.internal.configuration.domain.Configuration;
+import org.apache.geode.management.internal.configuration.utils.XmlUtils;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.SerializableRunnableIF;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+import org.apache.geode.test.junit.rules.VMProvider;
+
+public class CreateDataSourceCommandDUnitTest {
+
+ private static MemberVM locator, server1, server2;
+
+ @ClassRule
+ public static ClusterStartupRule cluster = new ClusterStartupRule();
+
+ @ClassRule
+ public static GfshCommandRule gfsh = new GfshCommandRule();
+
+
+ @BeforeClass
+ public static void before() throws Exception {
+ locator = cluster.startLocatorVM(0);
+ server1 = cluster.startServerVM(1, "group1", locator.getPort());
+ server2 = cluster.startServerVM(2, "group1", locator.getPort());
+
+ gfsh.connectAndVerify(locator);
+ }
+
+ @Test
+ public void testCreateDataSource() throws Exception {
+ VMProvider.invokeInEveryMember(
+ () ->
assertThat(JNDIInvoker.getNoOfAvailableDataSources()).isEqualTo(0));
+
+ // create the data-source
+ gfsh.executeAndAssertThat(
+ "create data-source --name=jndi1 --username=myuser --password=mypass
--pooled=false --url=\"jdbc:derby:newDB;create=true\"
--properties={'name':'name1','value':'value1'},{'name':'name2','value':'value2'}")
+ .statusIsSuccess().tableHasColumnOnlyWithValues("Member", "server-1",
"server-2");
+
+ // verify cluster config is updated
+ locator.invoke(() -> {
+ InternalConfigurationPersistenceService ccService =
+ ClusterStartupRule.getLocator().getConfigurationPersistenceService();
+ Configuration configuration = ccService.getConfiguration("cluster");
+ String xmlContent = configuration.getCacheXmlContent();
+
+ Document document = XmlUtils.createDocumentFromXml(xmlContent);
+ NodeList jndiBindings = document.getElementsByTagName("jndi-binding");
+
+ assertThat(jndiBindings.getLength()).isEqualTo(1);
+ assertThat(xmlContent).contains("user-name=\"myuser\"");
+ assertThat(xmlContent).contains("password=\"mypass\"");
+
+ boolean found = false;
+ for (int i = 0; i < jndiBindings.getLength(); i++) {
+ Element eachBinding = (Element) jndiBindings.item(i);
+ if (eachBinding.getAttribute("jndi-name").equals("jndi1")) {
+ found = true;
+ break;
+ }
+ }
+ assertThat(found).isTrue();
+ });
+
+ // verify datasource exists
+ VMProvider.invokeInEveryMember(
+ () ->
assertThat(JNDIInvoker.getNoOfAvailableDataSources()).isEqualTo(1));
+
+ // bounce server1
+ server1.stop(false);
+ server1 = cluster.startServerVM(1, locator.getPort());
+
+ // verify it has recreated the datasource from cluster config
+ server1.invoke(() -> {
+ assertThat(JNDIInvoker.getNoOfAvailableDataSources()).isEqualTo(1);
+ });
+
+ verifyThatNonExistentClassCausesGfshToError();
+ }
+
+ private void verifyThatNonExistentClassCausesGfshToError() {
+ SerializableRunnableIF IgnoreClassNotFound = () -> {
+ IgnoredException ex =
+ new IgnoredException("non_existent_class_name");
+ LogService.getLogger().info(ex.getAddMessage());
+ };
+
+ server1.invoke(IgnoreClassNotFound);
+ server2.invoke(IgnoreClassNotFound);
+
+ // create the binding
+ gfsh.executeAndAssertThat(
+ "create data-source --name=jndiBad --username=myuser --password=mypass
--pooled --pooled-data-source-factory-class=non_existent_class_name
--url=\"jdbc:derby:newDB;create=true\"")
+ .statusIsError();
+ }
+}
diff --git
a/geode-core/src/main/java/org/apache/geode/cache/configuration/JndiBindingsType.java
b/geode-core/src/main/java/org/apache/geode/cache/configuration/JndiBindingsType.java
index 044a7f6..aa58456 100644
---
a/geode-core/src/main/java/org/apache/geode/cache/configuration/JndiBindingsType.java
+++
b/geode-core/src/main/java/org/apache/geode/cache/configuration/JndiBindingsType.java
@@ -25,6 +25,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import org.apache.geode.annotations.Experimental;
@@ -237,6 +238,9 @@ public class JndiBindingsType {
@XmlAttribute(name = "xa-datasource-class")
protected String xaDatasourceClass;
+ @XmlTransient
+ private boolean createdByDataSourceCommand;
+
/**
* Gets the value of the configProperties property.
*
@@ -602,6 +606,14 @@ public class JndiBindingsType {
return getJndiName();
}
+ public boolean isCreatedByDataSourceCommand() {
+ return createdByDataSourceCommand;
+ }
+
+ public void setCreatedByDataSourceCommand(boolean
createdByDataSourceCommand) {
+ this.createdByDataSourceCommand = createdByDataSourceCommand;
+ }
+
/**
* <p>
@@ -649,6 +661,11 @@ public class JndiBindingsType {
this.configPropertyValue = value;
}
+ public ConfigProperty(String name, String value) {
+ this.configPropertyName = name;
+ this.configPropertyValue = value;
+ }
+
/**
* Get the id of the element. The id is the same as the name.
*
diff --git
a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
new file mode 100644
index 0000000..afe48ef
--- /dev/null
+++
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateDataSourceCommand.java
@@ -0,0 +1,176 @@
+/*
+ * 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.management.internal.cli.commands;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.logging.log4j.Logger;
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+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.distributed.DistributedMember;
+import
org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
+import org.apache.geode.internal.logging.LogService;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.SingleGfshCommand;
+import
org.apache.geode.management.internal.cli.commands.CreateJndiBindingCommand.DATASOURCE_TYPE;
+import
org.apache.geode.management.internal.cli.exceptions.EntityExistsException;
+import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
+import
org.apache.geode.management.internal.cli.functions.CreateJndiBindingFunction;
+import org.apache.geode.management.internal.cli.i18n.CliStrings;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class CreateDataSourceCommand extends SingleGfshCommand {
+ private static final Logger logger = LogService.getLogger();
+
+ static final String CREATE_DATA_SOURCE = "create data-source";
+ static final String CREATE_DATA_SOURCE__HELP = "Create a JDBC data source.";
+ static final String POOLED_DATA_SOURCE_FACTORY_CLASS =
"pooled-data-source-factory-class";
+ static final String POOLED_DATA_SOURCE_FACTORY_CLASS__HELP =
+ "This class will be created, by calling its no-arg constructor, and used
as the pool of this data source. "
+ + " The class must implement
org.apache.geode.datasource.PooledDataSourceFactory.";
+ static final String URL = "url";
+ static final String URL__HELP =
+ "This is the JDBC data source URL string, for example,
jdbc:hsqldb:hsql://localhost:1701.";
+ static final String NAME = "name";
+ static final String NAME__HELP = "Name of the data source to be created.";
+ static final String PASSWORD = "password";
+ static final String PASSWORD__HELP =
+ "This element specifies the default password used when creating a new
connection.";
+ static final String USERNAME = "username";
+ static final String USERNAME__HELP =
+ "This element specifies the default username used when creating a new
connection.";
+ static final String POOLED = "pooled";
+ static final String POOLED__HELP =
+ "By default a pooled data source is created. If this option is false
then a non-pooled data source is created.";
+ static final String IFNOTEXISTS__HELP =
+ "Skip the create operation when a data source with the same name already
exists. Without specifying this option, this command execution results into an
error.";
+ static final String PROPERTIES = "properties";
+ static final String PROPERTIES_HELP =
+ "Properties for the data source. When used with a pooled data source,
these properties will be used to configure the database data source unless the
name begins with \"pool.\"."
+ + " If that prefix is used it will be used to configure the pool
data source. For non-pooled data sources, these properties are just passed to
the database data source. "
+ + "The value is a comma separated list of json strings. Each json
string contains a name and value. "
+ + "For example:
--properties={'name':'name1','value':'value1'},{'name':'name2','value':'value2'}";
+
+ @CliCommand(value = CREATE_DATA_SOURCE, help = CREATE_DATA_SOURCE__HELP)
+ @CliMetaData(relatedTopic = CliStrings.TOPIC_GEODE_REGION,
+ interceptor =
"org.apache.geode.management.internal.cli.commands.UsernamePasswordInterceptor")
+ @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
+ operation = ResourcePermission.Operation.MANAGE)
+ public ResultModel createJDNIBinding(
+ @CliOption(key = POOLED_DATA_SOURCE_FACTORY_CLASS,
+ help = POOLED_DATA_SOURCE_FACTORY_CLASS__HELP) String
connectionPooledDatasource,
+ @CliOption(key = URL, mandatory = true,
+ help = URL__HELP) String url,
+ @CliOption(key = NAME, mandatory = true, help = NAME__HELP) String
jndiName,
+ @CliOption(key = USERNAME, help = USERNAME__HELP) String username,
+ @CliOption(key = PASSWORD, help = PASSWORD__HELP) String password,
+ @CliOption(key = CliStrings.IFNOTEXISTS, help = IFNOTEXISTS__HELP,
+ specifiedDefaultValue = "true", unspecifiedDefaultValue = "false")
boolean ifNotExists,
+ @CliOption(key = POOLED, help = POOLED__HELP,
+ specifiedDefaultValue = "true", unspecifiedDefaultValue = "true")
boolean pooled,
+ @CliOption(key = PROPERTIES, optionContext =
"splittingRegex=,(?![^{]*\\})",
+ help = PROPERTIES_HELP) DataSourceProperty[] properties) {
+
+ JndiBindingsType.JndiBinding configuration = new
JndiBindingsType.JndiBinding();
+ configuration.setCreatedByDataSourceCommand(true);
+ configuration.setConnPooledDatasourceClass(connectionPooledDatasource);
+ configuration.setConnectionUrl(url);
+ configuration.setJndiName(jndiName);
+ configuration.setPassword(password);
+ if (pooled) {
+ configuration.setType(DATASOURCE_TYPE.POOLED.getType());
+ } else {
+ configuration.setType(DATASOURCE_TYPE.SIMPLE.getType());
+ }
+ configuration.setUserName(username);
+ if (properties != null && properties.length > 0) {
+ // TODO convert DataSourceProperty to ConfigProperty
+ configuration.getConfigProperties().addAll(Arrays.asList(properties));
+ }
+
+ InternalConfigurationPersistenceService service =
+ (InternalConfigurationPersistenceService)
getConfigurationPersistenceService();
+
+ if (service != null) {
+ CacheConfig cacheConfig = service.getCacheConfig("cluster");
+ if (cacheConfig != null) {
+ JndiBindingsType.JndiBinding existing =
+ CacheElement.findElement(cacheConfig.getJndiBindings(), jndiName);
+ if (existing != null) {
+ throw new EntityExistsException(
+ CliStrings.format("Data source named \"{0}\" already exists.",
jndiName),
+ ifNotExists);
+ }
+ }
+ }
+
+ Set<DistributedMember> targetMembers = findMembers(null, null);
+ if (targetMembers.size() > 0) {
+ List<CliFunctionResult> jndiCreationResult = executeAndGetFunctionResult(
+ new CreateJndiBindingFunction(), configuration, targetMembers);
+ ResultModel result =
ResultModel.createMemberStatusResult(jndiCreationResult);
+ result.setConfigObject(configuration);
+ return result;
+ } else {
+ return ResultModel.createInfo("No members found.");
+ }
+ }
+
+ @Override
+ public void updateClusterConfig(String group, CacheConfig config, Object
element) {
+ config.getJndiBindings().add((JndiBindingsType.JndiBinding) element);
+ }
+
+ public static class DataSourceProperty {
+ @Override
+ public String toString() {
+ return "DataSourceProperty [name=" + name + ", value=" + value + "]";
+ }
+
+ private String name;
+ private String value;
+
+ public DataSourceProperty() {}
+
+ public DataSourceProperty(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ }
+}
diff --git
a/geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/DataSourcePropertyConverter.java
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/DataSourcePropertyConverter.java
new file mode 100644
index 0000000..9db292f
--- /dev/null
+++
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/converters/DataSourcePropertyConverter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.management.internal.cli.converters;
+
+import java.io.IOException;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.DataSourceProperty;
+
+/***
+ * Converter for CreateDataSourceCommand's --properties option.
+ *
+ */
+public class DataSourcePropertyConverter
+ implements Converter<DataSourceProperty> {
+
+ private static ObjectMapper mapper = new ObjectMapper();
+ static {
+ mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+ }
+
+ @Override
+ public boolean supports(Class<?> type, String optionContext) {
+ return DataSourceProperty.class.isAssignableFrom(type);
+ }
+
+ @Override
+ public DataSourceProperty convertFromText(String value,
+ Class<?> targetType, String optionContext) {
+ try {
+ return mapper.readValue(value, DataSourceProperty.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("invalid json: \"" + value + "\"
details: " + e);
+ }
+ }
+
+ @Override
+ public boolean getAllPossibleValues(List<Completion> completions, Class<?>
targetType,
+ String existingData, String optionContext, MethodTarget target) {
+ return false;
+ }
+}
diff --git
a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/CreateJndiBindingFunction.java
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/CreateJndiBindingFunction.java
index d9ed342..97ffb2b 100644
---
a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/CreateJndiBindingFunction.java
+++
b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/CreateJndiBindingFunction.java
@@ -36,31 +36,37 @@ import
org.apache.geode.management.internal.cli.i18n.CliStrings;
public class CreateJndiBindingFunction extends
CliFunction<JndiBindingsType.JndiBinding> {
private static final Logger logger = LogService.getLogger();
- private static final String RESULT_MESSAGE = "Created jndi binding \"{0}\"
on \"{1}\".";
@Override
public CliFunctionResult
executeFunction(FunctionContext<JndiBindingsType.JndiBinding> context)
throws DataSourceCreateException, NamingException {
ResultSender<Object> resultSender = context.getResultSender();
JndiBindingsType.JndiBinding configuration = context.getArguments();
+ final String TYPE_NAME;
+ if (configuration.isCreatedByDataSourceCommand()) {
+ TYPE_NAME = "data-source";
+ } else {
+ TYPE_NAME = "jndi-binding";
+ }
try {
JNDIInvoker.mapDatasource(getParamsAsMap(configuration),
convert(configuration.getConfigProperties()));
} catch (DataSourceCreateException ex) {
if (logger.isErrorEnabled()) {
- logger.error("create jndi-binding failed", ex.getWrappedException());
+ logger.error("create " + TYPE_NAME + " failed",
ex.getWrappedException());
}
throw ex;
} catch (NamingException ex) {
if (logger.isErrorEnabled()) {
- logger.error("create jndi-binding failed", ex);
+ logger.error("create " + TYPE_NAME + " failed", ex);
}
throw ex;
}
return new CliFunctionResult(context.getMemberName(), true,
- CliStrings.format(RESULT_MESSAGE, configuration.getJndiName(),
context.getMemberName()));
+ CliStrings.format("Created " + TYPE_NAME + " \"{0}\" on \"{1}\".",
+ configuration.getJndiName(), context.getMemberName()));
}
static Map getParamsAsMap(JndiBindingsType.JndiBinding binding) {