diff --git a/common/src/main/java/com/google/udmi/util/CommandLineOption.java b/common/src/main/java/com/google/udmi/util/CommandLineOption.java new file mode 100644 index 000000000..4753f3ef5 --- /dev/null +++ b/common/src/main/java/com/google/udmi/util/CommandLineOption.java @@ -0,0 +1,36 @@ +package com.google.udmi.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to indicate a command-line argument. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface CommandLineOption { + + String NO_VALUE = ""; + + /** + * Set the text help description. + */ + String description(); + + /** + * Short form argument. + */ + String short_form() default NO_VALUE; + + /** + * Long form argument. + */ + String long_form() default NO_VALUE; + + /** + * Set the argument type description. + */ + String arg_type() default NO_VALUE; +} diff --git a/common/src/main/java/com/google/udmi/util/CommandLineProcessor.java b/common/src/main/java/com/google/udmi/util/CommandLineProcessor.java new file mode 100644 index 000000000..287468f6d --- /dev/null +++ b/common/src/main/java/com/google/udmi/util/CommandLineProcessor.java @@ -0,0 +1,146 @@ +package com.google.udmi.util; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.udmi.util.GeneralUtils.ifNotNullThen; +import static com.google.udmi.util.GeneralUtils.ifNotNullThrow; +import static java.lang.String.CASE_INSENSITIVE_ORDER; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TreeMap; + +/** + * Simple command line parser class. + */ +public class CommandLineProcessor { + + private static final String TERMINATING_ARG = "--"; + private static final String EQUALS_SIGN = "="; + private static final int LINUX_SUCCESS_CODE = 0; + private static final int LINUX_ERROR_CODE = -1; + private final Object target; + private final Method showHelpMethod; + + Map optionMap = new TreeMap<>( + (a, b) -> CASE_INSENSITIVE_ORDER.compare(getSortArg(a), getSortArg(b))); + + private static String getSortArg(CommandLineOption k) { + String shortForm = k.short_form(); + return shortForm.isBlank() ? k.long_form().substring(1, 2) : shortForm.substring(1, 2); + } + + /** + * Create a new command line processor for the given target object. + */ + public CommandLineProcessor(Object target) { + this.target = target; + + showHelpMethod = getShowHelpMethod(); + optionMap.put(getShowHelpOption(), showHelpMethod); + + List methods = new ArrayList<>(List.of(target.getClass().getDeclaredMethods())); + methods.forEach(method -> ifNotNullThen(method.getAnnotation(CommandLineOption.class), + a -> optionMap.put(a, method))); + optionMap.values().forEach(method -> method.setAccessible(true)); + } + + private Method getShowHelpMethod() { + try { + return CommandLineProcessor.class.getDeclaredMethod("showUsage"); + } catch (Exception e) { + throw new RuntimeException("While getting showHelp method", e); + } + } + + private CommandLineOption getShowHelpOption() { + return showHelpMethod.getAnnotation(CommandLineOption.class); + } + + @CommandLineOption(short_form = "-h", description = "Show help and exit") + private void showUsage() { + showUsage(null); + } + + /** + * Show program usage. + */ + public void showUsage(String message) { + ifNotNullThen(message, m -> System.err.println(m)); + System.err.println("Options supported:"); + optionMap.forEach((option, method) -> System.err.printf(" %s %1s %s%n", + option.short_form(), option.arg_type(), option.description())); + System.exit(message == null ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE); + } + + /** + * Process the given arg list. Return a list of remaining arguments (if any). + */ + public List processArgs(List argList) { + try { + while (!argList.isEmpty()) { + String arg = argList.remove(0); + if (arg.equals(TERMINATING_ARG)) { + return argList; + } + Optional> first = optionMap.entrySet().stream() + .filter(option -> processArgEntry(arg, option.getKey(), option.getValue(), argList)) + .findFirst(); + if (first.isEmpty()) { + throw new IllegalArgumentException("Unrecognized command line option '" + arg + "'"); + } + if (!arg.startsWith("-")) { + argList.add(0, arg); + return argList; + } + } + return null; + } catch (Exception e) { + showUsage(e.getMessage()); + return null; + } + } + + private boolean processArgEntry(String arg, CommandLineOption option, Method method, + List argList) { + try { + if (arg.equals(option.short_form())) { + if (method.equals(showHelpMethod)) { + showUsage(); + } else if (requiresArg(method)) { + checkState(!option.arg_type().isBlank(), "Option with argument missing type parameter"); + String parameter = argList.remove(0); + method.invoke(target, parameter); + } else { + method.invoke(target); + } + return true; + } else if (!option.long_form().isBlank() && arg.startsWith(option.long_form())) { + throw new IllegalArgumentException("Long form command line not yet supported"); + } else if (option.short_form().isBlank() && option.long_form().isBlank()) { + throw new IllegalArgumentException( + "Neither long nor short form not defined for " + method.getName()); + } else if (!arg.startsWith("-")) { + return true; + } + return false; + } catch (Exception e) { + throw new IllegalArgumentException("Processing command line method " + method.getName(), e); + } + } + + private void cleanExit(boolean success) { + System.exit(success ? LINUX_SUCCESS_CODE : LINUX_ERROR_CODE); + } + + private boolean requiresArg(Method method) { + Type[] genericParameterTypes = method.getGenericParameterTypes(); + checkState(genericParameterTypes.length <= 1, + "expected <= 1 parameter for command line method %s", method.getName()); + return genericParameterTypes.length == 1; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/google/udmi/util/GeneralUtils.java b/common/src/main/java/com/google/udmi/util/GeneralUtils.java index 130de086b..820226745 100644 --- a/common/src/main/java/com/google/udmi/util/GeneralUtils.java +++ b/common/src/main/java/com/google/udmi/util/GeneralUtils.java @@ -660,11 +660,18 @@ public static String prefixedDifference(String prefix, Set a, Set argList, String description) { if (argList.isEmpty()) { - throw new RuntimeException(format("Missing required %s argument", description)); + throw new IllegalArgumentException(format("Missing required %s argument", description)); } return argList.remove(0); } + public static String removeStringArg(List argList, String description) { + if (!argList.isEmpty() && argList.get(0).startsWith("-")) { + throw new IllegalArgumentException(format("Missing required %s string argument", description)); + } + return removeArg(argList, description); + } + public static byte[] getFileBytes(String dataFile) { Path dataPath = Paths.get(dataFile); try { diff --git a/common/src/main/java/com/google/udmi/util/SiteModel.java b/common/src/main/java/com/google/udmi/util/SiteModel.java index 4e7b21745..5698b199b 100644 --- a/common/src/main/java/com/google/udmi/util/SiteModel.java +++ b/common/src/main/java/com/google/udmi/util/SiteModel.java @@ -18,6 +18,7 @@ import static com.google.udmi.util.GeneralUtils.ifNotNullThen; import static com.google.udmi.util.GeneralUtils.ifNullThen; import static com.google.udmi.util.GeneralUtils.removeArg; +import static com.google.udmi.util.GeneralUtils.removeStringArg; import static com.google.udmi.util.GeneralUtils.sha256; import static com.google.udmi.util.JsonUtil.asMap; import static com.google.udmi.util.JsonUtil.convertTo; @@ -150,7 +151,7 @@ private static void loadVersionInfo(ExecutionConfiguration exeConfig) { } public SiteModel(String toolName, List argList) { - this(removeArg(argList, "site_model"), projectSpecSupplier(argList), null); + this(removeStringArg(argList, "site_model"), projectSpecSupplier(argList), null); ExecutionConfiguration executionConfiguration = getExecutionConfiguration(); File outFile = new File(CONFIG_OUT_DIR, format("%s_conf.json", toolName)); System.err.println("Writing reconciled configuration file to " + outFile.getAbsolutePath()); @@ -167,11 +168,16 @@ public SiteModel(ExecutionConfiguration executionConfiguration) { private static Supplier projectSpecSupplier(List argList) { return () -> { - String nextArg = argList.isEmpty() ? "" : argList.get(0); - if (nextArg.startsWith("-") && !NO_SITE.equals(nextArg)) { + if (argList.isEmpty()) { + throw new IllegalArgumentException("Missing required project spec argument"); + } + String nextArg = argList.get(0); + if (nextArg.equals(NO_SITE)) { + return argList.remove(0); + } else if (nextArg.startsWith("-")) { return null; } - return removeArg(argList, "project_spec"); + return removeStringArg(argList, "project spec"); }; } diff --git a/validator/build.gradle b/validator/build.gradle index 1257f0a19..e1d46d2f6 100644 --- a/validator/build.gradle +++ b/validator/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:7.1.2' + classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' } } diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java index d87d9658d..40c172839 100644 --- a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java @@ -19,7 +19,6 @@ import static com.google.udmi.util.GeneralUtils.ifTrueGet; import static com.google.udmi.util.GeneralUtils.ifTrueThen; import static com.google.udmi.util.GeneralUtils.isTrue; -import static com.google.udmi.util.GeneralUtils.removeArg; import static com.google.udmi.util.GeneralUtils.writeString; import static com.google.udmi.util.JsonUtil.JSON_SUFFIX; import static com.google.udmi.util.JsonUtil.OBJECT_MAPPER; @@ -53,6 +52,8 @@ import com.google.daq.mqtt.util.ExceptionMap.ErrorTree; import com.google.daq.mqtt.util.PubSubPusher; import com.google.daq.mqtt.util.ValidationError; +import com.google.udmi.util.CommandLineOption; +import com.google.udmi.util.CommandLineProcessor; import com.google.udmi.util.Common; import com.google.udmi.util.SiteModel; import java.io.File; @@ -126,6 +127,7 @@ public class Registrar { private final Map schemas = new HashMap<>(); private final String generation = getGenerationString(); private final Set summarizers = new HashSet<>(); + private final CommandLineProcessor commandLineProcessor = new CommandLineProcessor(this); private CloudIotManager cloudIotManager; private File schemaBase; private PubSubPusher updatePusher; @@ -200,57 +202,33 @@ Registrar processArgs(List argListRaw) { // Add implicit NO_SITE site spec for local-only site model processing. argList.add(NO_SITE); } - siteModel = new SiteModel(TOOL_NAME, argList); + try { + siteModel = new SiteModel(TOOL_NAME, argList); + } catch (IllegalArgumentException e) { + commandLineProcessor.showUsage(e.getMessage()); + } processProfile(siteModel.getExecutionConfiguration()); return postProcessArgs(argList); } Registrar postProcessArgs(List argList) { + List remainingArgs = commandLineProcessor.processArgs(argList); + ifNotNullThen(remainingArgs, this::setDeviceList); requireNonNull(siteModel, "siteModel not defined"); - while (argList.size() > 0) { - String option = argList.remove(0); - switch (option) { - case "-r" -> setToolRoot(removeArg(argList, "tool root")); - case "-p" -> setProjectId(removeArg(argList, "project id")); - case "-s" -> setSitePath(removeArg(argList, "site path")); - case "-a" -> setTargetRegistry(removeArg(argList, "alt registry")); - case "-e" -> setRegistrySuffix(removeArg(argList, "registry suffix")); - case "-f" -> setFeedTopic(removeArg(argList, "feed topic")); - case "-u" -> setUpdateFlag(true); - case "-b" -> setBlockUnknown(true); - case "-l" -> setIdleLimit(removeArg(argList, "idle limit")); - case "-t" -> setValidateMetadata(false); - case "-q" -> setQueryOnly(true); - case "-d" -> setDeleteDevices(true); - case "-x" -> setExpungeDevices(true); - case "-n" -> setRunnerThreads(removeArg(argList, "runner threads")); - case "-c" -> setCreateRegistries(removeArg(argList, "create registries")); - case "--" -> { - setDeviceList(argList); - return this; - } - default -> { - if (option.startsWith("-")) { - throw new RuntimeException("Unknown cmdline option " + option); - } - // Add the current non-option back into the list and use it as device names list. - argList.add(0, option); - setDeviceList(argList); - return this; - } - } - } return this; } - private void setQueryOnly(boolean queryOnly) { - this.queryOnly = queryOnly; + @CommandLineOption(short_form = "-q", description = "Query only") + private void setQueryOnly() { + this.queryOnly = true; } - private void setBlockUnknown(boolean block) { - blockUnknown = block; + @CommandLineOption(short_form = "-b", description = "Block unknown devices") + private void setBlockUnknown() { + blockUnknown = true; } + @CommandLineOption(short_form = "-c", description = "Create registries") private void setCreateRegistries(String registrySpec) { try { createRegistries = Integer.parseInt(registrySpec); @@ -262,23 +240,28 @@ private void setCreateRegistries(String registrySpec) { } } + @CommandLineOption(short_form = "-n", arg_type = "n", + description = "Set number of runner threads") private void setRunnerThreads(String argValue) { runnerThreads = Integer.parseInt(argValue); } - private void setDeleteDevices(boolean deleteDevices) { - this.deleteDevices = deleteDevices; + @CommandLineOption(short_form = "-d", description = "Delete (known) devices") + private void setDeleteDevices() { checkNotNull(projectId, "delete devices specified with no target project"); - this.updateCloudIoT |= deleteDevices; + this.deleteDevices = true; + this.updateCloudIoT = true; } - private void setExpungeDevices(boolean expungeDevices) { - this.expungeDevices = expungeDevices; + @CommandLineOption(short_form = "-x", description = "Expunge (unknown) devices") + private void setExpungeDevices() { checkNotNull(projectId, "expunge devices specified with no target project"); - this.updateCloudIoT |= expungeDevices; + this.expungeDevices = true; + this.updateCloudIoT = true; } - void setRegistrySuffix(String suffix) { + @CommandLineOption(short_form = "-e", arg_type = "s", description = "Set registry suffix") + private void setRegistrySuffix(String suffix) { siteModel.getExecutionConfiguration().registry_suffix = suffix; } @@ -287,7 +270,7 @@ private void processProfile(ExecutionConfiguration config) { setSitePath(config.site_model); setProjectId(config.project_id); if (config.project_id != null) { - setUpdateFlag(true); + setUpdateFlag(); } } @@ -355,17 +338,20 @@ private void createRegistrySuffix(String suffix) { System.err.println("Created registry " + registry); } + @CommandLineOption(short_form = "-l", arg_type = "t", description = "Set idle limit") private void setIdleLimit(String option) { idleLimit = Duration.parse("PT" + option); System.err.println("Limiting devices to duration " + idleLimit.toSeconds() + "s"); } - private void setUpdateFlag(boolean update) { - updateCloudIoT = update; + @CommandLineOption(short_form = "-u", description = "Update cloud") + private void setUpdateFlag() { + updateCloudIoT = true; } - private void setValidateMetadata(boolean validateMetadata) { - this.doValidate = validateMetadata; + @CommandLineOption(short_form = "-t", description = "Do not validate metadata") + private void setValidateMetadata() { + this.doValidate = false; } private void setDeviceList(List deviceList) { @@ -373,6 +359,7 @@ private void setDeviceList(List deviceList) { blockUnknown = false; } + @CommandLineOption(short_form = "-f", arg_type = "s", description = "Set PubSub feed topic") private void setFeedTopic(String feedTopic) { System.err.println("Sending device feed to topic " + feedTopic); feedPusher = new PubSubPusher(projectId, feedTopic); @@ -440,7 +427,8 @@ private SetupUdmiConfig getCloudVersionInfo() { return ifNotNullGet(cloudIotManager, CloudIotManager::getVersionInformation); } - protected void setSitePath(String sitePath) { + @CommandLineOption(short_form = "-s", arg_type = "s", description = "Set site path") + private void setSitePath(String sitePath) { checkNotNull(SCHEMA_NAME, "schemaName not set yet"); siteDir = new File(sitePath); siteModel = ofNullable(siteModel).orElseGet(() -> new SiteModel(sitePath)); @@ -1167,7 +1155,8 @@ private void initializeDevices(Map localDevices) { }); } - protected void setProjectId(String projectId) { + @CommandLineOption(short_form = "-p", arg_type = "s", description = "Set project id") + private void setProjectId(String projectId) { if (NO_SITE.equals(projectId) || projectId == null) { this.projectId = null; return; @@ -1178,7 +1167,8 @@ protected void setProjectId(String projectId) { this.projectId = projectId; } - protected void setToolRoot(String toolRoot) { + @CommandLineOption(short_form = "-r", arg_type = "s", description = "Set tool root") + private void setToolRoot(String toolRoot) { try { schemaBase = new File(toolRoot, SCHEMA_BASE_PATH); if (!schemaBase.isDirectory()) { @@ -1258,7 +1248,8 @@ public List getMockActions() { return cloudIotManager.getMockActions(); } - public void setTargetRegistry(String altRegistry) { + @CommandLineOption(short_form = "-a", arg_type = "s", description = "Set alternate registry") + private void setTargetRegistry(String altRegistry) { siteModel.getExecutionConfiguration().registry_id = altRegistry; siteModel.getExecutionConfiguration().alt_registry = null; }