From 5a5f4ef0848985fdeac77f74ad5b3e3972165ba3 Mon Sep 17 00:00:00 2001 From: jianwu chen Date: Mon, 7 Aug 2023 23:38:14 -0700 Subject: [PATCH] Add name index for TDNode for large number of chilrens; Improve CliArg --- .../java/org/jsonex/cliarg/CLIParser.java | 33 ++++++++------ .../main/java/org/jsonex/cliarg/CLISpec.java | 4 +- .../main/java/org/jsonex/cliarg/Param.java | 23 +++++----- .../java/org/jsonex/cliarg/CliParserTest.java | 4 +- .../main/java/org/jsonex/treedoc/TDNode.java | 44 +++++++++++++++---- .../java/org/jsonex/treedoc/TDNodeTest.java | 22 ++++++++++ 6 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 treedoc/src/test/java/org/jsonex/treedoc/TDNodeTest.java diff --git a/CliArg/src/main/java/org/jsonex/cliarg/CLIParser.java b/CliArg/src/main/java/org/jsonex/cliarg/CLIParser.java index 783c386..27ff251 100644 --- a/CliArg/src/main/java/org/jsonex/cliarg/CLIParser.java +++ b/CliArg/src/main/java/org/jsonex/cliarg/CLIParser.java @@ -1,20 +1,26 @@ package org.jsonex.cliarg; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.jsonex.core.util.BeanConvertContext; import org.jsonex.core.util.ClassUtil; +import static org.jsonex.core.util.LangUtil.doIf; +import static org.jsonex.core.util.LangUtil.doIfNotNull; +import static org.jsonex.core.util.ListUtil.isIn; import org.jsonex.jsoncoder.JSONCoder; import org.jsonex.jsoncoder.JSONCoderOption; import org.jsonex.treedoc.TDNode.Type; import org.jsonex.treedoc.json.TDJSONOption; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import java.util.*; - -import static org.jsonex.core.util.LangUtil.doIf; -import static org.jsonex.core.util.ListUtil.isIn; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Parse the input command line arguments against the {@link CLISpec}. The parsed result will be stored in the target @@ -106,7 +112,7 @@ private void parseArg(String arg) { } Param param = spec.indexedParams.get(paramIndex++); missingParams.remove(param.name); - param.property.set(target, parseValue(param, arg)); + doIfNotNull(parseValue(param, arg), v -> param.property.set(target, v)); } private Object parseValue(Param param, String value) { @@ -124,7 +130,8 @@ private Object parseValue(Param param, String value) { ? JSONCoder.decode(value, cls, opt) : JSONCoder.decodeTo(value, param.getProperty().get(target), opt.setMergeArray(true)); } catch (Exception e) { - log.error("Error parsing parameter:" + param.name, e); + errorMessages.put(param.name, value + ";" + e.toString()); + // log.error("Error parsing parameter:" + param.name, e); } return null; } @@ -133,9 +140,9 @@ private Object parseValue(Param param, String value) { public String getErrorsAsString() { StringBuilder sb = new StringBuilder(); - doIf(!missingParams.isEmpty(), () -> sb.append("\nMissing required arguments:" + missingParams)); - doIf(!extraArgs.isEmpty(), () -> sb.append("\nUnexpected arguments:" + extraArgs)); - doIf(!errorMessages.isEmpty(), () -> sb.append("\nError parsing following arguments:" + errorMessages)); + doIf(!missingParams.isEmpty(), () -> sb.append("\nMissing required arguments:").append(missingParams)); + doIf(!extraArgs.isEmpty(), () -> sb.append("\nUnexpected arguments:").append(extraArgs)); + doIf(!errorMessages.isEmpty(), () -> sb.append("\nError parsing following arguments:").append(errorMessages)); return sb.toString(); } } diff --git a/CliArg/src/main/java/org/jsonex/cliarg/CLISpec.java b/CliArg/src/main/java/org/jsonex/cliarg/CLISpec.java index ed069f8..b4155b4 100644 --- a/CliArg/src/main/java/org/jsonex/cliarg/CLISpec.java +++ b/CliArg/src/main/java/org/jsonex/cliarg/CLISpec.java @@ -20,6 +20,7 @@ import static org.jsonex.core.util.ListUtil.setAt; /** + *
  * CLI specification based on annotated java bean of `cls`. Following annotations will be processed:
  *
  * Class level:
@@ -27,9 +28,10 @@
  *   {@link Summary}: Summary of the command (Optional)
  *   {@link Description}: Description of the command (Optional)
  *   {@link Examples}: Array of string representation of samples usages (Optional)
- *
  * For field level annotations, please refer to class {@link Param}
  *
+ * 
+ * * @param */ @Data diff --git a/CliArg/src/main/java/org/jsonex/cliarg/Param.java b/CliArg/src/main/java/org/jsonex/cliarg/Param.java index 54abbda..6cadbbc 100644 --- a/CliArg/src/main/java/org/jsonex/cliarg/Param.java +++ b/CliArg/src/main/java/org/jsonex/cliarg/Param.java @@ -11,27 +11,28 @@ import static org.jsonex.core.util.StringUtil.noNull; /** - * Represent an command line parameter, it can be either argument or option - * If index is not null indicate it's argument - * Argument default to required unless explicitly specified. - * required argument can't follow non-required argument which index less than it - * If index is null indicates it's an option, option default to not required, unless specified - * - * For option of Boolean type, it will be mapped as flag, that means the value of the option can be omitted. * + *
+ * Represent a command line parameter, it can be either argument or option
+ * If index is not null indicate it is an argument.
+ *    Argument default to required unless explicitly specified.
+ *    Required argument can't follow non-required argument which index less than it
+ * If index is null indicates it's an option, option default to not required, unless specified
+ *    For option of Boolean type, it will be mapped as flag, that means the value of the option can be omitted.
+
  * For Param of complex type or array/list, the value can be specified as JSON(ex) string, the top level "{" or "[",
  * can be, omitted. The quote for key and value can be omitted.
- *
- * For array parameters, it also possible to specify the values as separate options. The values will be merged
- *
+
+ * For array parameters, it is also possible to specify the values as separate options. The values will be merged
+
  * Following Annotation will be processed for each parameter:
- *
  * {@link Name}  Name of the parameter, optional, default to field name
  * {@link ShortName}  The optional short name
  * {@link Description}  The optional description
  * {@link Index}  Indicate this an indexed parameter
  * {@link Required}  Indicate if this field is required. all the index fields are required unless explicitly indicated.
  *     All the non-index fields are not required unless explicitly indicated.
+ * 
*/ @Data public class Param { diff --git a/CliArg/src/test/java/org/jsonex/cliarg/CliParserTest.java b/CliArg/src/test/java/org/jsonex/cliarg/CliParserTest.java index a3ed5a7..2bee19b 100644 --- a/CliArg/src/test/java/org/jsonex/cliarg/CliParserTest.java +++ b/CliArg/src/test/java/org/jsonex/cliarg/CliParserTest.java @@ -49,7 +49,7 @@ public static class Arg1 { @Test public void testParse() { - CLISpec spec = new CLISpec(Arg1.class); + CLISpec spec = new CLISpec<>(Arg1.class); assertMatchesSnapshot("spec", spec); log.info("spec:\n" + spec.printUsage()); @@ -57,7 +57,7 @@ public void testParse() { String[] args = { "abc", "10", "name:n1,x:1,y:2", "-o", "VAL2", "--optInt", "100", "--arrayArg", "str1,str2,'It\\'s escapted'", "--arrayArg", "array as separate option"}; - CLIParser parser = spec.parse(args, 0); + CLIParser parser = spec.parse(args, 0); log.info("parsedValue:\n" + parser.target); assertMatchesSnapshot("parserTarget", parser.target); diff --git a/treedoc/src/main/java/org/jsonex/treedoc/TDNode.java b/treedoc/src/main/java/org/jsonex/treedoc/TDNode.java index 0a7a9ce..02dbf37 100644 --- a/treedoc/src/main/java/org/jsonex/treedoc/TDNode.java +++ b/treedoc/src/main/java/org/jsonex/treedoc/TDNode.java @@ -15,25 +15,31 @@ import lombok.experimental.Accessors; import org.jsonex.core.charsource.Bookmark; import org.jsonex.core.type.Lazy; +import static org.jsonex.core.util.LangUtil.orElse; +import static org.jsonex.core.util.LangUtil.safe; import org.jsonex.core.util.ListUtil; +import static org.jsonex.core.util.ListUtil.last; +import static org.jsonex.core.util.ListUtil.listOf; +import static org.jsonex.core.util.ListUtil.map; import org.jsonex.core.util.StringUtil; import org.jsonex.treedoc.TDPath.Part; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; -import static org.jsonex.core.util.LangUtil.orElse; -import static org.jsonex.core.util.LangUtil.safe; -import static org.jsonex.core.util.ListUtil.*; - /** A Node in TreeDoc */ @RequiredArgsConstructor // @Getter @Setter @Accessors(chain = true) public class TDNode { + private final static int SIZE_TO_INIT_NAME_INDEX = 64; public final static String ID_KEY = "$id"; public final static String REF_KEY = "$ref"; @@ -58,6 +64,7 @@ public enum Type { MAP, ARRAY, SIMPLE } transient private boolean deduped; transient private final Lazy hash = new Lazy<>(); transient private final Lazy str = new Lazy<>(); + transient private Map nameIndex; // Will only initialize when size is big enough public TDNode(TDNode parent, String key) { this.doc = parent.doc; this.parent = parent; this.key = key; } public TDNode(TreeDoc doc, String key) { this.doc = doc; this.key = key; } @@ -101,9 +108,20 @@ public TDNode addChild(TDNode node) { if (node.key == null) // Assume it's array element node.key = "" + getChildrenSize(); children.add(node); + if (children.size() > SIZE_TO_INIT_NAME_INDEX && nameIndex == null) + initNameIndex(); return touch(); } + private void initNameIndex() { + nameIndex = new HashMap<>(); + for (int i = 0; i< children.size(); i++) { + TDNode child = children.get(i); + if (child.key != null) + nameIndex.put(child.key, i); + } + } + public void swapWith(TDNode to) { if (this.parent == null || to.parent == null) throw new IllegalArgumentException("Can't swap root node"); @@ -129,8 +147,12 @@ public TDNode getChild(String name) { return idx < 0 ? null : children.get(idx); } - int indexOf(TDNode node) { return ListUtil.indexOf(children, n -> n == node); } - int indexOf(String name) { return ListUtil.indexOf(children, n -> n.getKey().equals(name)); } + int indexOf(TDNode node) { + return nameIndex != null ? indexOf(node.key) : ListUtil.indexOf(children, n -> n == node); + } + int indexOf(String name) { + return nameIndex != null ? orElse(nameIndex.get(name), -1) : ListUtil.indexOf(children, n -> n.getKey().equals(name)); + } int index() { return parent == null ? 0 : parent.indexOf(this); } public Object getChildValue(String name) { @@ -206,8 +228,8 @@ public List getPath() { public boolean isLeaf() { return getChildrenSize() == 0; } private TDNode touch() { - hash.clear();; - str.clear();; + hash.clear(); + str.clear(); if (parent != null) parent.touch(); return this; @@ -285,15 +307,19 @@ public List getChildrenKeys() { if (this.type == Type.SIMPLE || children == null) return result; // Add the key column + Set keySet = new HashSet<>(); result.add(COLUMN_KEY); + keySet.add(COLUMN_KEY); boolean hasValue = false; for (TDNode c : children) { if (c.value != null) hasValue = true; if (c.children != null) for (TDNode cc : c.getChildren()) - if (!result.contains(cc.key)) + if (!keySet.contains(cc.key)) { result.add(cc.key); + keySet.add(cc.key); + } } if (hasValue) result.add(1, COLUMN_VALUE); diff --git a/treedoc/src/test/java/org/jsonex/treedoc/TDNodeTest.java b/treedoc/src/test/java/org/jsonex/treedoc/TDNodeTest.java new file mode 100644 index 0000000..a29213e --- /dev/null +++ b/treedoc/src/test/java/org/jsonex/treedoc/TDNodeTest.java @@ -0,0 +1,22 @@ +package org.jsonex.treedoc; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +import java.util.List; + +public class TDNodeTest { + @Test + public void test() { + TDNode node = new TDNode().setType(TDNode.Type.ARRAY); + long start = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + node.createChild("name_" + i).setType(TDNode.Type.MAP).createChild("name_" + i + "_1").setValue("value_" + i + "_1"); + } + List keys = node.getChildrenKeys(); + long time = System.currentTimeMillis() - start; + System.out.println(time); + assertTrue(time < 2000); +// Assert.assertEquals("", keys.toString()); + } +}