diff --git a/README-X.md b/README-X.md index ae137af..bd95a19 100644 --- a/README-X.md +++ b/README-X.md @@ -87,4 +87,10 @@ position. A *YamlConfig* supports the extension of missing keys from another instance, which is a way to migrate existing configuration files to a newer version by adding new key-value pairs. Existing sections are extended, missing sections are added. Keys are never deleted, -as that could possibly delete still needed configuration information for the user. \ No newline at end of file +as that could possibly delete still needed configuration information for the user. + +### Commented-Out Keys + +Keys which are commented out are detected if their corresponding comment is within the right mapping-block and starts with +any amount of whitespace, followed by the key's name, followed by a colon `:`. Since it is a feature of this mapper to comment +out keys in order to cause null-values in Sections, in order to thereby deactivate messages/features, these keys will not be extended. diff --git a/README.md b/README.md index 7c6de69..6f8f56b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ An object mapper for key-based configurations making use of [GPEEE](https://gith - [Value Interpretation](#value-interpretation) - [Comments](#comments) - [Key Extension](#key-extension) + - [Commented-Out Keys](#commented-out-keys) ## Merging @@ -94,4 +95,10 @@ position. A *YamlConfig* supports the extension of missing keys from another instance, which is a way to migrate existing configuration files to a newer version by adding new key-value pairs. Existing sections are extended, missing sections are added. Keys are never deleted, -as that could possibly delete still needed configuration information for the user. \ No newline at end of file +as that could possibly delete still needed configuration information for the user. + +### Commented-Out Keys + +Keys which are commented out are detected if their corresponding comment is within the right mapping-block and starts with +any amount of whitespace, followed by the key's name, followed by a colon `:`. Since it is a feature of this mapper to comment +out keys in order to cause null-values in Sections, in order to thereby deactivate messages/features, these keys will not be extended. diff --git a/src/main/java/me/blvckbytes/bbconfigmapper/YamlConfig.java b/src/main/java/me/blvckbytes/bbconfigmapper/YamlConfig.java index 26812e0..dc5ee09 100644 --- a/src/main/java/me/blvckbytes/bbconfigmapper/YamlConfig.java +++ b/src/main/java/me/blvckbytes/bbconfigmapper/YamlConfig.java @@ -35,11 +35,13 @@ import org.yaml.snakeyaml.comments.CommentLine; import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.nodes.*; import org.yaml.snakeyaml.representer.Representer; import java.io.*; import java.util.*; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; @@ -271,6 +273,86 @@ private void executeWhileMergedTuplesAbsent(Runnable executable) { } } + private @Nullable NodeTuple findTupleNodeRecursively( + Node node, + Predicate matchPredicate, + Predicate skipPredicate + ) { + NodeTuple result; + + if (node instanceof MappingNode) { + if (skipPredicate.test((MappingNode) node)) + return null; + + for (NodeTuple entry : ((MappingNode) node).getValue()) { + if (matchPredicate.test(entry)) + return entry; + + if ((result = findTupleNodeRecursively(entry.getValueNode(), matchPredicate, skipPredicate)) != null) + return result; + } + } + + else if (node instanceof SequenceNode) { + for (Node entry : ((SequenceNode) node).getValue()) { + if ((result = findTupleNodeRecursively(entry, matchPredicate, skipPredicate)) != null) + return result; + } + } + + return null; + } + + private boolean isKeyCommentedOut(String key, Node container) { + // BlockComment-s are members to their successive nodes + // For mappings, in the list of tuples, on the ScalarNode-s of the keys + // For lists, on the ScalarNode-s of items + // If a comment is at the end of the file, it'll be an EndComment on the root MappingNode + + Predicate commentedKeyMatcher = (comment) -> comment.getValue().trim().startsWith(key + ": "); + + // Possibly, a key which is not the last has been commented out + if (container instanceof MappingNode) { + for (NodeTuple containerTuple : ((MappingNode) container).getValue()) { + if (containerTuple.getKeyNode().getBlockComments().stream().anyMatch(commentedKeyMatcher)) + return true; + } + } + + // Possibly, the last key has been commented out + + Mark containerStart = container.getStartMark(); + + if (containerStart != null) { + // Find the first node that's after the container and either on same indent or less (maybe + // that's redundant) and skip over recursing down the current container. + NodeTuple nextTuple = findTupleNodeRecursively( + rootNode, + currentNode -> { + Mark otherStart = currentNode.getKeyNode().getStartMark(); + + return ( + otherStart.getLine() > containerStart.getLine() && + otherStart.getColumn() <= containerStart.getColumn() + ); + }, + currentContainer -> currentContainer == container + ); + + // Next node available - look at the next node's comments + if (nextTuple != null) { + if (nextTuple.getKeyNode().getBlockComments().stream().anyMatch(commentedKeyMatcher)) + return true; + } + } + + // No next-node available (EOF) - wrap around to root node + + List endComments = rootNode.getEndComments(); + + return endComments != null && endComments.stream().anyMatch(commentedKeyMatcher); + } + /** * Extends keys which the provided config contains but are absent on this instance * by copying over the values those keys hold @@ -285,9 +367,19 @@ public int extendMissingKeys(YamlConfig other) { if (this.exists(pathOfTuple)) return false; + String key = ((ScalarNode) tuple.getKeyNode()).getValue(); + int lastPathDotIndex = pathOfTuple.lastIndexOf('.'); + + if (lastPathDotIndex > 0) { + String parentPath = pathOfTuple.substring(0, lastPathDotIndex); + Node parentNode = locateNode(parentPath, false, false).a; + + if (isKeyCommentedOut(key, parentNode)) + return false; + } + MappingNode container = locateContainerNode(pathOfTuple, true).a; List containerTuples = container.getValue(); - String key = ((ScalarNode) tuple.getKeyNode()).getValue(); // The new key is at an index which doesn't yet exist, add to the end of the tuple list if (indexOfTuple >= containerTuples.size()) { @@ -647,8 +739,10 @@ private NodeTuple createNewTuple(@Nullable Node keyNode, @Nullable String key, N // Try to reuse already present key nodes Node tupleKey = keyValueTuple == null ? null : keyValueTuple.getKeyNode(); + List mappingTuples = mapping.getValue(); + mappingTuples.remove(keyValueTuple); keyValueTuple = createNewTuple(tupleKey, pathPart, createNewMappingNode(null)); - mapping.getValue().add(keyValueTuple); + mappingTuples.add(keyValueTuple); // Invalidate the (null) cache for this newly added tuple invalidateLocateKeyCacheFor(mapping, pathPart); diff --git a/src/test/java/me/blvckbytes/bbconfigmapper/YamlConfigExtensionTests.java b/src/test/java/me/blvckbytes/bbconfigmapper/YamlConfigExtensionTests.java index 6496ebf..6373777 100644 --- a/src/test/java/me/blvckbytes/bbconfigmapper/YamlConfigExtensionTests.java +++ b/src/test/java/me/blvckbytes/bbconfigmapper/YamlConfigExtensionTests.java @@ -57,4 +57,18 @@ public void shouldExtendKeysWithScalarValues() throws Exception { helper.assertSave("mappings_patched.yml", baseConfig); } + + @Test + public void shouldNotExtendCommentedKeys() throws Exception { + char[] caseSuffixes = {'a', 'b', 'c'}; + + for (char caseSuffix : caseSuffixes) { + YamlConfig baseConfig = helper.makeConfig("commented_keys_base_" + caseSuffix + ".yml"); + YamlConfig extensionConfig = helper.makeConfig("commented_keys_extension.yml"); + + baseConfig.extendMissingKeys(extensionConfig); + + helper.assertSave("commented_keys_save_" + caseSuffix + ".yml", baseConfig); + } + } } diff --git a/src/test/resources/commented_keys_base_a.yml b/src/test/resources/commented_keys_base_a.yml new file mode 100644 index 0000000..059dfc3 --- /dev/null +++ b/src/test/resources/commented_keys_base_a.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 +# e: 4 + f: 5 +g: + h: 2 \ No newline at end of file diff --git a/src/test/resources/commented_keys_base_b.yml b/src/test/resources/commented_keys_base_b.yml new file mode 100644 index 0000000..059dfc3 --- /dev/null +++ b/src/test/resources/commented_keys_base_b.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 +# e: 4 + f: 5 +g: + h: 2 \ No newline at end of file diff --git a/src/test/resources/commented_keys_base_c.yml b/src/test/resources/commented_keys_base_c.yml new file mode 100644 index 0000000..08b6aef --- /dev/null +++ b/src/test/resources/commented_keys_base_c.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 + e: 4 + f: 5 +g: + # h: 2 \ No newline at end of file diff --git a/src/test/resources/commented_keys_extension.yml b/src/test/resources/commented_keys_extension.yml new file mode 100644 index 0000000..441a0bd --- /dev/null +++ b/src/test/resources/commented_keys_extension.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 + e: 4 + f: 5 +g: + h: 2 diff --git a/src/test/resources/save/commented_keys_save_a.yml b/src/test/resources/save/commented_keys_save_a.yml new file mode 100644 index 0000000..593659a --- /dev/null +++ b/src/test/resources/save/commented_keys_save_a.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 + # e: 4 + f: 5 +g: + h: 2 diff --git a/src/test/resources/save/commented_keys_save_b.yml b/src/test/resources/save/commented_keys_save_b.yml new file mode 100644 index 0000000..593659a --- /dev/null +++ b/src/test/resources/save/commented_keys_save_b.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 + # e: 4 + f: 5 +g: + h: 2 diff --git a/src/test/resources/save/commented_keys_save_c.yml b/src/test/resources/save/commented_keys_save_c.yml new file mode 100644 index 0000000..3dcff3d --- /dev/null +++ b/src/test/resources/save/commented_keys_save_c.yml @@ -0,0 +1,8 @@ +a: + b: 1 + c: 2 + d: 3 + e: 4 + f: 5 +g: +# h: 2 \ No newline at end of file