From 1cb818e3cbd6154762f908be8698317fa10fbc49 Mon Sep 17 00:00:00 2001 From: "adheli.tavares" Date: Fri, 9 Apr 2021 11:51:05 +0100 Subject: Add command line handler Added a Handler for command line classes to share common strucutures. Issue-ID: POLICY-3128 Change-Id: I662911c467faf5c39b8db018bb1a564fba7587a6 Signed-off-by: adheli.tavares --- utils/pom.xml | 5 + .../utils/cmd/CommandLineArgumentsHandler.java | 273 +++++++++++++++++++++ .../common/utils/cmd/CommandLineException.java | 54 ++++ .../common/utils/cmd/TestCommandLineArguments.java | 181 ++++++++++++++ .../common/utils/resources/ResourceUtilsTest.java | 4 +- .../src/test/resources/cmdFiles/configuration.json | 4 + utils/src/test/resources/cmdFiles/property.json | 3 + utils/src/test/resources/version.txt | 1 + 8 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineArgumentsHandler.java create mode 100644 utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineException.java create mode 100644 utils/src/test/java/org/onap/policy/common/utils/cmd/TestCommandLineArguments.java create mode 100644 utils/src/test/resources/cmdFiles/configuration.json create mode 100644 utils/src/test/resources/cmdFiles/property.json create mode 100644 utils/src/test/resources/version.txt diff --git a/utils/pom.xml b/utils/pom.xml index bd9df71a..75173a2a 100644 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -3,6 +3,7 @@ ONAP Policy Engine - Common Modules ================================================================================ Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + Modifications Copyright (C) 2021 Nordix Foundation. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -117,5 +118,9 @@ assertj-core test + + commons-cli + commons-cli + diff --git a/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineArgumentsHandler.java b/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineArgumentsHandler.java new file mode 100644 index 00000000..37a9047b --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineArgumentsHandler.java @@ -0,0 +1,273 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.cmd; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.utils.resources.ResourceUtils; + +/** + * Class for command line common processing. + * + * @author Adheli Tavares (adheli.tavares@est.tech) + * + */ +public class CommandLineArgumentsHandler { + private static final String FILE_MESSAGE_PREAMBLE = " file \""; + private static final int HELP_LINE_LENGTH = 120; + + private final Options options; + + private final String helpClassName; + private final String component; + + @Getter + @Setter + private String configurationFilePath = null; + + @Getter + @Setter + private String propertyFilePath = null; + + @Getter + private CommandLine commandLine = null; + + /** + * Construct the options with default values for the CLI editor. + */ + protected CommandLineArgumentsHandler(String helpClassName, String component) { + this.helpClassName = helpClassName; + this.component = component; + //@formatter:off + options = new Options(); + options.addOption(Option.builder("h") + .longOpt("help") + .desc("outputs the usage of this command") + .required(false) + .type(Boolean.class) + .build()); + options.addOption(Option.builder("v") + .longOpt("version") + .desc("outputs the version of " + this.component) + .required(false) + .type(Boolean.class) + .build()); + options.addOption(Option.builder("c") + .longOpt("config-file") + .desc(String.format("the full path to the configuration file to use, " + + "the configuration file must be a Json file containing the %s parameters", this.component)) + .hasArg().argName("CONFIG_FILE") + .required(false) + .type(String.class) + .build()); + //@formatter:on + } + + /** + * Construct the options for the CLI editor with extra options. + */ + public CommandLineArgumentsHandler(String helpClassName, String component, Option... customOptions) { + this(helpClassName, component); + if (customOptions != null && customOptions.length > 0) { + for (Option option : customOptions) { + options.addOption(option); + } + } + } + + /** + * Construct the options with brand new options for the CLI editor. + */ + public CommandLineArgumentsHandler(String helpClassName, String component, Options options) { + this.options = options; + this.helpClassName = helpClassName; + this.component = component; + } + + /** + * Parse the command line options. + * + * @param args The command line arguments + * @return a string with a message for help and version, or null if there is no message + * @throws CommandLineException on command argument errors + */ + public String parse(final String[] args) throws CommandLineException { + // Clear all our arguments + setConfigurationFilePath(null); + setPropertyFilePath(null); + + try { + commandLine = new DefaultParser().parse(options, args); + } catch (final ParseException | NullPointerException e) { + throw new CommandLineException("invalid command line arguments specified", e); + } + + // Arguments left over after Commons CLI does its stuff + final String[] remainingArgs = removeEmptyValues(commandLine.getArgs()); + + if (remainingArgs.length > 0) { + throw new CommandLineException("too many command line arguments specified: " + Arrays.toString(args)); + } + + if (commandLine.hasOption('h')) { + return help(); + } + + if (commandLine.hasOption('v')) { + return version(); + } + + if (commandLine.hasOption('c')) { + setConfigurationFilePath(commandLine.getOptionValue('c')); + } + + if (commandLine.hasOption('p')) { + setPropertyFilePath(commandLine.getOptionValue('p')); + } + + return null; + } + + /** + * Validate the command line options. + * + * @throws CommandLineException on command argument validation errors + */ + public void validate() throws CommandLineException { + validateReadableFile(this.component + " configuration", configurationFilePath); + } + + /** + * Print version information for policy distribution. + * + * @return the version string + */ + public String version() { + return ResourceUtils.getResourceAsString("version.txt"); + } + + /** + * Print help information for policy distribution. + * + * @return the help string + */ + public String help() { + final HelpFormatter helpFormatter = new HelpFormatter(); + final StringWriter stringWriter = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(stringWriter); + final String cmdLineSyntax = this.helpClassName + " [options...]"; + + helpFormatter.printHelp(printWriter, HELP_LINE_LENGTH, cmdLineSyntax, "options", options, 0, 0, ""); + + return stringWriter.toString(); + } + + /** + * Gets the full expanded configuration file path. + * + * @return the configuration file path + */ + public String getFullConfigurationFilePath() { + return ResourceUtils.getFilePath4Resource(getConfigurationFilePath()); + } + + /** + * Check set configuration file path. + * + * @return true, if check set configuration file path + */ + public boolean checkSetConfigurationFilePath() { + return StringUtils.isNotBlank(getConfigurationFilePath()); + } + + /** + * Gets the full expanded property file path. + * + * @return the property file path + */ + public String getFullPropertyFilePath() { + return ResourceUtils.getFilePath4Resource(getPropertyFilePath()); + } + + /** + * Check set property file path. + * + * @return true, if check set property file path + */ + public boolean checkSetPropertyFilePath() { + return StringUtils.isNotBlank(getPropertyFilePath()); + } + + /** + * Validate readable file. + * + * @param fileTag the file tag + * @param fileName the file name + * @throws CommandLineException on the file name passed as a parameter + */ + protected void validateReadableFile(final String fileTag, final String fileName) throws CommandLineException { + if (StringUtils.isBlank(fileName)) { + throw new CommandLineException(fileTag + " file was not specified as an argument"); + } + + // The file name refers to a resource on the local file system + final URL fileUrl = ResourceUtils.getUrl4Resource(fileName); + if (fileUrl == null) { + throw new CommandLineException(fileTag + FILE_MESSAGE_PREAMBLE + fileName + "\" does not exist"); + } + + try { + Path path = Path.of(fileUrl.toURI()); + if (!Files.isRegularFile(path)) { + throw new CommandLineException(fileTag + FILE_MESSAGE_PREAMBLE + fileName + "\" is not a normal file"); + } + if (!Files.isReadable(path)) { + throw new CommandLineException(fileTag + FILE_MESSAGE_PREAMBLE + fileName + "\" is unreadable"); + } + } catch (URISyntaxException e) { + throw new CommandLineException("Error parsing " + fileUrl.toString(), e); + } + + } + + /** + * Checks if args has any null or empty value after parsing. + * + * @param args remaining args from CLI parse. + */ + private String[] removeEmptyValues(String[] args) { + return Arrays.stream(args).filter(StringUtils::isNotBlank).toArray(String[]::new); + } +} diff --git a/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineException.java b/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineException.java new file mode 100644 index 00000000..95870f75 --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/cmd/CommandLineException.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.cmd; + +/** + * Exception used for CommandLineArguments class. + * + * @author Adheli Tavares (adheli.tavares@est.tech) + * + */ +public class CommandLineException extends Exception { + + /** + * Generated serialVersionUID. + */ + private static final long serialVersionUID = -1200607308084606425L; + + /** + * Instantiates a new exception with a message. + * + * @param message the message + */ + public CommandLineException(final String message) { + super(message); + } + + /** + * Instantiates a new exception with a message and a caused by exception. + * + * @param message the message + * @param exp the exception that caused this exception to be thrown + */ + public CommandLineException(final String message, final Exception exp) { + super(message, exp); + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/cmd/TestCommandLineArguments.java b/utils/src/test/java/org/onap/policy/common/utils/cmd/TestCommandLineArguments.java new file mode 100644 index 00000000..1501e769 --- /dev/null +++ b/utils/src/test/java/org/onap/policy/common/utils/cmd/TestCommandLineArguments.java @@ -0,0 +1,181 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.cmd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.junit.Test; + +public class TestCommandLineArguments { + private static final String FAKE_HELP_CLASS = "org.onap.policy.HelpClass"; + private static final String FAKE_COMPONENT = "fake policy cpm"; + private static final String TEST_CONFIG_FILE = "cmdFiles/configuration.json"; + private static final String TEST_PROPERTY_FILE = "cmdFiles/property.json"; + private static final String ERR_MSG_INVALID_ARGS = "invalid command line arguments specified"; + private static final String ERR_MSG_POLICY_CONFIG_FILE = + "fake policy cpm configuration file was not specified as an argument"; + + CommandLineArgumentsHandler testCmd = new CommandLineArgumentsHandler(FAKE_HELP_CLASS, FAKE_COMPONENT); + + @Test + public void testVersion() throws CommandLineException { + String[] version = {"-v"}; + assertThat(testCmd.parse(version)).startsWith("ONAP Version test."); + } + + @Test + public void testHelp() throws CommandLineException { + String[] help = {"-h"}; + assertThat(testCmd.parse(help)).startsWith("usage: org.onap.policy.HelpClass [options...]"); + } + + @Test + public void testParse() throws CommandLineException { + String[] args = {"-c", TEST_CONFIG_FILE}; + testCmd.parse(args); + + assertTrue(testCmd.checkSetConfigurationFilePath()); + assertThat(testCmd.getFullConfigurationFilePath()).contains(TEST_CONFIG_FILE); + } + + @Test + public void testParse_ShouldThrowExceptionWithInvalidArguments() { + String[] invalidArgs = {"-a"}; + assertThatThrownBy(() -> testCmd.parse(invalidArgs)).hasMessage(ERR_MSG_INVALID_ARGS) + .hasRootCauseMessage("Unrecognized option: -a"); + } + + @Test + public void testParse_ShouldThrowExceptionWithExtraArguments() { + String[] remainingArgs = {"-c", TEST_CONFIG_FILE, "extraArgs"}; + String expectedErrorMsg = + "too many command line arguments specified: [-c, cmdFiles/configuration.json, extraArgs]"; + assertThatThrownBy(() -> testCmd.parse(remainingArgs)).hasMessage(expectedErrorMsg); + } + + @Test + public void testParse_ShouldThrowExceptionWhenFileNameNull() { + String[] nullArgs = {"-c", null}; + assertThatThrownBy(() -> testCmd.parse(nullArgs)).hasMessage(ERR_MSG_INVALID_ARGS).hasRootCauseMessage(null); + } + + @Test + public void testValidate() throws CommandLineException { + String[] validConfigArgs = {"-c", TEST_CONFIG_FILE}; + testCmd.parse(validConfigArgs); + assertThatCode(() -> testCmd.validate()).doesNotThrowAnyException(); + } + + @Test + public void testValidate_ShouldThrowExceptionWhenConfigFileNotPresent() throws CommandLineException { + String[] versionArgs = {"-v"}; + testCmd.parse(versionArgs); + assertValidate(versionArgs, ERR_MSG_POLICY_CONFIG_FILE); + } + + @Test + public void testValidate_ShouldThrowExceptionWhenFileNameEmpty() { + String[] argsOnlyKeyNoValue = {"-c", ""}; + assertValidate(argsOnlyKeyNoValue, ERR_MSG_POLICY_CONFIG_FILE); + assertFalse(testCmd.checkSetConfigurationFilePath()); + } + + @Test + public void testValidate_ShouldThrowExceptionWhenFileNameEmptySpace() { + String[] argsOnlyKeyNoValue = {"-c", " "}; + assertValidate(argsOnlyKeyNoValue, ERR_MSG_POLICY_CONFIG_FILE); + assertFalse(testCmd.checkSetConfigurationFilePath()); + } + + @Test + public void testValidate_ShouldThrowExceptionWhenFileNameDoesNotExist() { + String[] fileNameNotExistentArgs = {"-c", "someFileName.json"}; + assertValidate(fileNameNotExistentArgs, + "fake policy cpm configuration file \"someFileName.json\" does not exist"); + } + + @Test + public void testValidate_ShouldThrowExceptionWhenFileNameIsNotFile() { + String[] folderAsFileNameArgs = {"-c", "src/test/resources"}; + assertValidate(folderAsFileNameArgs, + "fake policy cpm configuration file \"src/test/resources\" is not a normal file"); + } + + @Test + public void testAddExtraOptions() throws CommandLineException { + Option extra = Option.builder("p").longOpt("property-file") + .desc("the full path to the topic property file to use, the property file contains the " + + FAKE_COMPONENT + " properties") + .hasArg().argName("PROP_FILE").required(false).type(String.class).build(); + + CommandLineArgumentsHandler testCmdExtraOpt = + new CommandLineArgumentsHandler(FAKE_HELP_CLASS, FAKE_COMPONENT, extra); + + String[] args = {"-p", TEST_PROPERTY_FILE}; + testCmdExtraOpt.parse(args); + + assertTrue(testCmdExtraOpt.checkSetPropertyFilePath()); + assertThat(testCmdExtraOpt.getFullPropertyFilePath()).contains(TEST_PROPERTY_FILE); + + String[] argsNoProperty = {"-p", ""}; + testCmdExtraOpt.parse(argsNoProperty); + + assertFalse(testCmdExtraOpt.checkSetPropertyFilePath()); + } + + @Test + public void testNewOptions() throws CommandLineException { + Options newOptions = new Options(); + newOptions.addOption( + Option.builder("a").longOpt("fake-option").desc("the fake property to check command line parse") + .hasArg().argName("FAKE_OPT").required(false).type(String.class).build()); + + CommandLineArgumentsHandler testCmdExtraOpt = + new CommandLineArgumentsHandler(FAKE_HELP_CLASS, FAKE_COMPONENT, newOptions); + + String[] args = {"-a", "aaaa"}; + testCmdExtraOpt.parse(args); + + assertTrue(testCmdExtraOpt.getCommandLine().hasOption("a")); + + // should raise exception as -c is not present on options; + // default options should've been replaced by constructor parameter. + String[] argsError = {"-c", "aaaa.json"}; + assertThatThrownBy(() -> testCmdExtraOpt.parse(argsError)).hasMessage(ERR_MSG_INVALID_ARGS) + .hasRootCauseMessage("Unrecognized option: -c"); + } + + private void assertValidate(String[] testArgs, String expectedErrorMsg) { + try { + testCmd.parse(testArgs); + } catch (CommandLineException e) { + fail(e.getMessage()); + } + assertThatThrownBy(() -> testCmd.validate()).hasMessage(expectedErrorMsg); + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java b/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java index 2e122187..6a2fe1ad 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java +++ b/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2018 Ericsson. All rights reserved. * Modifications Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. - * Modifications Copyright (C) 2020 Nordix Foundation. + * Modifications Copyright (C) 2020-2021 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -264,7 +264,7 @@ public class ResourceUtilsTest { theString = ResourceUtils.getResourceAsString(""); - assertEquals("logback-test.xml\nMETA-INF\norg\ntestdir\n", theString); + assertEquals("cmdFiles\nlogback-test.xml\nMETA-INF\norg\ntestdir\nversion.txt\n", theString); } diff --git a/utils/src/test/resources/cmdFiles/configuration.json b/utils/src/test/resources/cmdFiles/configuration.json new file mode 100644 index 00000000..64cd100c --- /dev/null +++ b/utils/src/test/resources/cmdFiles/configuration.json @@ -0,0 +1,4 @@ +{ + "propertyName" : "test", + "propertyType" : "string" +} diff --git a/utils/src/test/resources/cmdFiles/property.json b/utils/src/test/resources/cmdFiles/property.json new file mode 100644 index 00000000..be63ece4 --- /dev/null +++ b/utils/src/test/resources/cmdFiles/property.json @@ -0,0 +1,3 @@ +{ + "configName" : "test" +} diff --git a/utils/src/test/resources/version.txt b/utils/src/test/resources/version.txt new file mode 100644 index 00000000..9970c7b4 --- /dev/null +++ b/utils/src/test/resources/version.txt @@ -0,0 +1 @@ +ONAP Version test. -- cgit 1.2.3-korg