Repository: decorators-squad/eo-yaml
Pull request #595 (2024-01-22 17:20:17)

#594 Visitor pattern and YamlPrintVisitor
PR for #594 

* Introduced the ``YamlVisitor`` interface and scaffolding methods for implementing visiting of any ``YamlNode``;
* Implemented class ``YamlPrintVisitor`` and used it in for ``toString()`` instead of ``RtYamlPrinter``;


Patch:
diff --git a/src/main/java/com/amihaiemil/eoyaml/BaseYamlNode.java b/src/main/java/com/amihaiemil/eoyaml/BaseYamlNode.java
index b3d290e..d9cbbe9 100644
--- a/src/main/java/com/amihaiemil/eoyaml/BaseYamlNode.java
+++ b/src/main/java/com/amihaiemil/eoyaml/BaseYamlNode.java
@@ -29,7 +29,6 @@ package com.amihaiemil.eoyaml;
 
 import com.amihaiemil.eoyaml.exceptions.YamlPrintException;
 import com.amihaiemil.eoyaml.exceptions.YamlReadingException;
-import java.io.IOException;
 import java.io.StringWriter;
 
 /**
@@ -91,16 +90,55 @@ abstract class BaseYamlNode implements YamlNode {
      */
     @Override
     public final String toString() {
+        final String lineSeparator = System.lineSeparator();
+        final YamlVisitor<String> visitor = new YamlPrintVisitor(lineSeparator);
+        String toString = "";
+        if(this.type().equals(Node.SCALAR)) {
+            toString += "---" + lineSeparator;
+            toString += printPossibleComment(this, lineSeparator);
+            toString += this.accept(visitor);
+            toString += lineSeparator + "...";
+        } else {
+            toString += printPossibleComment(this, lineSeparator);
+            if(!toString.isEmpty()) {
+                toString += "---" + lineSeparator;
+            }
+            toString += this.accept(visitor);
+        }
+        return toString;
+    }
+
+    /**
+     * Print a comment. Make sure to split the lines if there are more
+     * lines separated by NewLine and also add a '# ' in front of each
+     * line.
+     * @param node Node containing the Comment.
+     * @param lineSeparator Line separator.
+     * @return Printed comment.
+     */
+    private String printPossibleComment(
+        final YamlNode node, final String lineSeparator
+    ) {
         final StringWriter writer = new StringWriter();
-        final YamlPrinter printer = new RtYamlPrinter(writer);
-        try {
-            printer.print(this);
-            return writer.toString();
-        } catch (final IOException ex) {
-            throw new YamlPrintException(
-                "IOException when printing YAML", ex
-            );
+        if(node != null && node.comment() != null) {
+            final Comment tmpComment;
+            if(node.comment() instanceof ScalarComment) {
+                tmpComment = ((ScalarComment) node.comment()).above();
+            } else {
+                tmpComment = node.comment();
+            }
+            final String com = tmpComment.value();
+            if (com.trim().length() != 0) {
+                String[] lines = com.split(lineSeparator);
+                for (final String line : lines) {
+                    writer
+                        .append("# ")
+                        .append(line)
+                        .append(lineSeparator);
+                }
+            }
         }
+        return writer.toString();
     }
 
 }
diff --git a/src/main/java/com/amihaiemil/eoyaml/RtYamlPrinter.java b/src/main/java/com/amihaiemil/eoyaml/RtYamlPrinter.java
index e608d0a..74efd76 100644
--- a/src/main/java/com/amihaiemil/eoyaml/RtYamlPrinter.java
+++ b/src/main/java/com/amihaiemil/eoyaml/RtYamlPrinter.java
@@ -39,6 +39,7 @@ import java.util.List;
  * @author Mihai Andronache (amihaiemil@gmail.com)
  * @version $Id$
  * @since 4.3.1
+ * @todo #594:30min Refactor this class to use {@link YamlPrintVisitor}.
  */
 final class RtYamlPrinter implements YamlPrinter {
 
diff --git a/src/main/java/com/amihaiemil/eoyaml/Scalar.java b/src/main/java/com/amihaiemil/eoyaml/Scalar.java
index cf11252..d3e4403 100644
--- a/src/main/java/com/amihaiemil/eoyaml/Scalar.java
+++ b/src/main/java/com/amihaiemil/eoyaml/Scalar.java
@@ -27,6 +27,9 @@
  */
 package com.amihaiemil.eoyaml;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Yaml Scalar.
  * @author Mihai Andronache (amihaiemil@gmail.com)
@@ -46,4 +49,9 @@ public interface Scalar extends YamlNode {
      *  supposed to be.
      */
     String value();
+
+    @Override
+    default List<YamlNode> children() {
+        return new ArrayList<>();
+    }
 }
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlMapping.java b/src/main/java/com/amihaiemil/eoyaml/YamlMapping.java
index f836999..eb2ce25 100644
--- a/src/main/java/com/amihaiemil/eoyaml/YamlMapping.java
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlMapping.java
@@ -461,4 +461,14 @@ public interface YamlMapping extends YamlNode {
         }
         return null;
     }
+
+    @Override
+    default List<YamlNode> children() {
+        final List<YamlNode> children = new ArrayList<>();
+        for (final YamlNode key : this.keys()) {
+            children.add(key);
+            children.add(this.value(key));
+        }
+        return children;
+    }
 }
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlNode.java b/src/main/java/com/amihaiemil/eoyaml/YamlNode.java
index b1a0401..e7037db 100644
--- a/src/main/java/com/amihaiemil/eoyaml/YamlNode.java
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlNode.java
@@ -29,6 +29,8 @@ package com.amihaiemil.eoyaml;
 
 import com.amihaiemil.eoyaml.exceptions.YamlReadingException;
 
+import java.util.List;
+
 /**
  * YAML node.
  * @author Mihai Andronache (amihaiemil@gmail.com)
@@ -110,4 +112,21 @@ public interface YamlNode extends Comparable<YamlNode> {
     <T extends YamlNode> T asClass(Class<T> clazz, Node type)
         throws YamlReadingException, ClassCastException;
 
+    /**
+     * Return the children of this node. If the list is empty, it means it's a
+     * terminal node.
+     * @return List of children, never null.
+     */
+    List<YamlNode> children();
+
+    /**
+     * Accept a YamlVisitor.
+     * @param visitor Visitor.
+     * @return Result of the visit.
+     * @param <T> Type of the result.
+     */
+    default <T> T accept(YamlVisitor<? extends T> visitor) {
+        return visitor.visitYamlNode(this);
+    }
+
 }
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlPrintVisitor.java b/src/main/java/com/amihaiemil/eoyaml/YamlPrintVisitor.java
new file mode 100644
index 0000000..a261dbc
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlPrintVisitor.java
@@ -0,0 +1,377 @@
+/**
+ * Copyright (c) 2016-2024, Mihai Emil Andronache
+ * All rights reserved.
+ * <p>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml;
+
+import java.io.StringWriter;
+import java.util.List;
+
+/**
+ * Visitor which prints the YAML in block format.
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @checkstyle ExecutableStatementCount (1000 lines)
+ * @since 7.2.0
+ */
+public final class YamlPrintVisitor implements YamlVisitor<String> {
+
+    /**
+     * How many indentation spaces to use?
+     */
+    private final int indentation;
+
+    /**
+     * Line separator.
+     */
+    private final String lineSeparator;
+
+    /**
+     * Ctor. Defaults to indentation 2 and the System line separator.
+     */
+    public YamlPrintVisitor() {
+        this(2, System.lineSeparator());
+    }
+
+    /**
+     * Ctor.
+     * @param indentation Number of indentation spaces.
+     */
+    public YamlPrintVisitor(final int indentation) {
+        this(indentation, System.lineSeparator());
+    }
+
+    /**
+     * Ctor.
+     * @param lineSeparator Line separator.
+     */
+    public YamlPrintVisitor(final String lineSeparator) {
+        this(2, lineSeparator);
+    }
+
+    /**
+     * Ctor.
+     * @param indentation Number of spaces to use for indentation.
+     * @param lineSeparator Line separator.
+     */
+    public YamlPrintVisitor(final int indentation, final String lineSeparator) {
+        this.indentation = indentation;
+        this.lineSeparator = lineSeparator;
+    }
+    @Override
+    public String visitYamlMapping(final YamlMapping node) {
+        final StringWriter writer = new StringWriter();
+        for (final YamlNode key : node.keys()) {
+            final YamlNode value = node.value(key);
+            writer.append(this.printPossibleComment(value));
+            if(key.type().equals(Node.SCALAR)) {
+                writer.append(this.visitYamlNode(key));
+            } else {
+                writer.append("?").append(this.lineSeparator);
+                final String printedValue = this.visitYamlNode(key);
+                writer.append(this.indent(printedValue, this.indentation));
+                writer.append(this.lineSeparator);
+            }
+            if(value == null || value.type().equals(Node.SCALAR)) {
+                writer.append(": ");
+                writer.append(this.visitYamlNode(value));
+            } else {
+                final String printedValue = this.visitYamlNode(value);
+                if("null".equals(printedValue) || "[]".equals(printedValue)
+                    || "{}".equals(printedValue)
+                ) {
+                    writer.append(": ");
+                    writer.append(printedValue);
+                } else {
+                    writer.append(":");
+                    writer.append(this.lineSeparator);
+                    writer.append(this.indent(printedValue, this.indentation));
+                }
+            }
+            writer.append(this.lineSeparator);
+        }
+        final String printedMapping = writer.toString();
+        final String trimmed;
+        if (printedMapping.length() > 0)  {
+            trimmed = printedMapping.substring(0, printedMapping.length() - 1);
+        } else {
+            trimmed = printedMapping;
+        }
+        return trimmed;
+    }
+
+    @Override
+    public String visitYamlSequence(final YamlSequence node) {
+        final StringWriter writer = new StringWriter();
+        for(final YamlNode value : node.values()) {
+            writer.append(this.printPossibleComment(value));
+            if(value == null || value.type().equals(Node.SCALAR)) {
+                writer.append("- ");
+                writer.append(this.visitYamlNode(value));
+            } else {
+                final String printedValue = this.visitYamlNode(value);
+                if("null".equals(printedValue) || "[]".equals(printedValue)
+                    || "{}".equals(printedValue)
+                ) {
+                    writer.append("- ");
+                    writer.append(printedValue);
+                } else {
+                    writer.append("-");
+                    writer.append(this.lineSeparator);
+                    writer.append(this.indent(printedValue, this.indentation));
+                }
+            }
+            writer.append(this.lineSeparator);
+        }
+        final String printedSequence = writer.toString();
+        final String trimmed;
+        if (printedSequence.length() > 0)  {
+            trimmed = printedSequence.substring(
+                0, printedSequence.length() - 1
+            );
+        } else {
+            trimmed = printedSequence;
+        }
+        return trimmed;
+    }
+
+    @Override
+    public String visitScalar(final Scalar node) {
+        final StringWriter writer = new StringWriter();
+        if (node instanceof BaseFoldedScalar) {
+            final BaseFoldedScalar foldedScalar = (BaseFoldedScalar) node;
+            writer.append(">");
+            if(!node.comment().value().isEmpty()) {
+                writer.append(" # ").append(node.comment().value());
+            }
+            writer.append(this.lineSeparator);
+            final List<String> unfolded = foldedScalar.unfolded();
+            for(int idx = 0; idx < unfolded.size(); idx++) {
+                writer.append(
+                    this.indent(
+                        unfolded.get(idx).trim(),
+                        indentation
+                    )
+                );
+                if(idx < unfolded.size() - 1) {
+                    writer.append(this.lineSeparator);
+                }
+            }
+        } else if (node instanceof RtYamlScalarBuilder.BuiltLiteralBlockScalar
+            || node instanceof ReadLiteralBlockScalar
+        ) {
+            writer.append("|");
+            if(!node.comment().value().isEmpty()) {
+                writer.append(" # ").append(node.comment().value());
+            }
+            writer
+                .append(this.lineSeparator)
+                .append(
+                    this.indent(node.value(), indentation)
+                );
+        } else {
+            writer.append(new Escaped(node).value());
+            final Comment comment = node.comment();
+            if (comment instanceof ScalarComment) {
+                final ScalarComment scalarComment = (ScalarComment) comment;
+                if (!scalarComment.inline().value().isEmpty()) {
+                    writer.append(" # ").append(
+                        scalarComment.inline().value()
+                    );
+                }
+            }
+        }
+        return writer.toString();
+    }
+
+    @Override
+    public String visitYamlStream(final YamlStream node) {
+        final StringWriter writer = new StringWriter();
+        node.forEach(
+            yaml -> writer.append("---")
+                        .append(this.lineSeparator)
+                        .append(
+                            this.indent(
+                                this.visitYamlNode(yaml),
+                                this.indentation
+                            )
+                        )
+                        .append(this.lineSeparator)
+        );
+        final String printedStream = writer.toString();
+        final String trimmed;
+        if (printedStream.length() > 0)  {
+            trimmed = printedStream.substring(0, printedStream.length() - 1);
+        } else {
+            trimmed = printedStream;
+        }
+        return trimmed;
+    }
+
+    @Override
+    public String visitYamlNode(final YamlNode node) {
+        final StringWriter writer = new StringWriter();
+        if (node == null || node.isEmpty()) {
+            if (node instanceof YamlSequence) {
+                writer.append("[]");
+            } else if (node instanceof YamlMapping) {
+                writer.append("{}");
+            } else if (node instanceof YamlStream) {
+                writer.append("---" + this.lineSeparator + "...");
+            } else {
+                writer.append("null");
+            }
+        } else {
+            if (node instanceof Scalar) {
+                writer.append(this.visitScalar((Scalar) node));
+            } else if (node instanceof YamlSequence) {
+                writer.append(this.visitYamlSequence((YamlSequence) node));
+            } else if (node instanceof YamlMapping) {
+                writer.append(this.visitYamlMapping((YamlMapping) node));
+            } else if (node instanceof YamlStream) {
+                writer.append(this.visitYamlStream((YamlStream) node));
+            }
+        }
+        return writer.toString();
+    }
+
+    @Override
+    public String defaultResult() {
+        return "";
+    }
+
+    @Override
+    public String aggregateResult(
+        final String aggregate, final String nextResult
+    ) {
+        return aggregate + nextResult;
+    }
+
+    /**
+     * Indent a text with a number of spaces.
+     * @param print Text to indent.
+     * @param numberOfSpaces Number of indentation spaces.
+     * @return Indented text.
+     */
+    private String indent(final String print, final int numberOfSpaces) {
+        int spaces = numberOfSpaces;
+        final StringBuilder indent = new StringBuilder();
+        while (spaces > 0) {
+            indent.append(" ");
+            spaces--;
+        }
+        final String[] lines = print.split(this.lineSeparator);
+        final StringWriter indented = new StringWriter();
+        for(final String line : lines) {
+            indented
+                .append(indent.toString())
+                .append(line)
+                .append(this.lineSeparator);
+        }
+        final String str = indented.toString();
+        return str.substring(0, str.length() - 1);
+    }
+
+    /**
+     * Print a comment. Make sure to split the lines if there are more
+     * lines separated by NewLine and also add a '# ' in front of each
+     * line.
+     * @param node Node containing the Comment.
+     * @return Printed comment.
+     */
+    private String printPossibleComment(final YamlNode node) {
+        final StringWriter writer = new StringWriter();
+        if(node != null && node.comment() != null) {
+            final Comment tmpComment;
+            if(node.comment() instanceof ScalarComment) {
+                tmpComment = ((ScalarComment) node.comment()).above();
+            } else {
+                tmpComment = node.comment();
+            }
+            final String com = tmpComment.value();
+            if (com.trim().length() != 0) {
+                String[] lines = com.split(this.lineSeparator);
+                for (final String line : lines) {
+                    writer
+                        .append("# ")
+                        .append(line)
+                        .append(this.lineSeparator);
+                }
+            }
+        }
+        return writer.toString();
+    }
+
+    /**
+     * A scalar which escapes its value.
+     * @author Mihai Andronache (amihaiemil@gmail.com)
+     * @version $Id$
+     * @since 7.2.0
+     * @checkstyle LineLength (100 lines)
+     */
+    static class Escaped extends BaseScalar {
+
+        /**
+         * Original unescaped scalar.
+         */
+        private final Scalar original;
+
+        /**
+         * Ctor.
+         * @param original Unescaped scalar.
+         */
+        Escaped(final Scalar original) {
+            this.original = original;
+        }
+
+        @Override
+        public String value() {
+            final String value = this.original.value();
+            String toEscape;
+            if(value == null) {
+                toEscape = "null";
+            } else {
+                toEscape = value;
+            }
+            boolean alreadyEscaped = (toEscape.startsWith("'") && toEscape.endsWith("'"))
+                || (toEscape.startsWith("\"") && toEscape.endsWith("\""));
+            final String needsEscaping = ".*[?\\-#:>|$%&{}\\[\\]@`!*,'\"]+.*|[ ]+|null";
+            if (!alreadyEscaped && toEscape.matches(needsEscaping)) {
+                if(toEscape.contains("\"")) {
+                    toEscape = "'" + toEscape + "'";
+                } else {
+                    toEscape = "\"" + toEscape + "\"";
+                }
+            }
+            return toEscape;
+        }
+
+        @Override
+        public Comment comment() {
+            return this.original.comment();
+        }
+    }
+}
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlSequence.java b/src/main/java/com/amihaiemil/eoyaml/YamlSequence.java
index be73e68..382d9ae 100644
--- a/src/main/java/com/amihaiemil/eoyaml/YamlSequence.java
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlSequence.java
@@ -30,9 +30,7 @@ package com.amihaiemil.eoyaml;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeParseException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
+import java.util.*;
 
 /**
  * A Yaml sequence.
@@ -49,6 +47,11 @@ public interface YamlSequence extends YamlNode, Iterable<YamlNode> {
      */
     Collection<YamlNode> values();
 
+    @Override
+    default List<YamlNode> children() {
+        return new ArrayList<>(this.values());
+    }
+
     /**
      * Returns this YamlSequence's children Iterator.<br><br>
      * It is equivalent to YamlSequence.values().iterator().
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlStream.java b/src/main/java/com/amihaiemil/eoyaml/YamlStream.java
index e4acbf3..5d167e2 100644
--- a/src/main/java/com/amihaiemil/eoyaml/YamlStream.java
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlStream.java
@@ -54,6 +54,11 @@ public interface YamlStream extends YamlNode, Stream<YamlNode> {
      */
     Collection<YamlNode> values();
 
+    @Override
+    default List<YamlNode> children() {
+        return new ArrayList<>(this.values());
+    }
+
     default Comment comment() {
         return new BuiltComment(this, "");
     }
@@ -218,6 +223,4 @@ public interface YamlStream extends YamlNode, Stream<YamlNode> {
     default Optional<YamlNode> findAny() {
         return this.values().stream().findAny();
     }
-
-
 }
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlVisitor.java b/src/main/java/com/amihaiemil/eoyaml/YamlVisitor.java
new file mode 100644
index 0000000..3250c7c
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlVisitor.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2016-2024, Mihai Emil Andronache
+ * All rights reserved.
+ * <p>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml;
+
+/**
+ * YAML Visitor. Use this interface to implement your own YamlVisitor, to visit
+ * all the nodes of a given YamlNode (sequence, mapping, stream etc). See class
+ * {@link YamlPrintVisitor} for an example.
+ * @param <T> The return type of each visit.
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @since 7.2.0
+ */
+public interface YamlVisitor<T> {
+
+    /**
+     * Visit a YAML Node.
+     * @param node YamlNode, never null.
+     * @return T returned type.
+     */
+    default T visitYamlNode(final YamlNode node) {
+        return this.visitChildren(node);
+    }
+
+    /**
+     * Visit a Scalar.
+     * @param node Scalar, never null.
+     * @return T returned type.
+     */
+    default T visitScalar(final Scalar node) {
+        return this.visitChildren(node);
+    }
+
+    /**
+     * Visit a YamlMapping.
+     * @param node YamlMapping, never null.
+     * @return T returned type.
+     */
+    default T visitYamlMapping(final YamlMapping node) {
+        return this.visitChildren(node);
+    }
+
+    /**
+     * Visit a YamlSequence.
+     * @param node YamlSequence, never null.
+     * @return T returned type.
+     */
+    default T visitYamlSequence(final YamlSequence node) {
+        return this.visitChildren(node);
+    }
+
+    /**
+     * Visit a YamlStream.
+     * @param node YamlStream, never null.
+     * @return T returned type.
+     */
+    default T visitYamlStream(final YamlStream node) {
+        return this.visitChildren(node);
+    }
+
+    /**
+     * Visit the children of a given YamlNode.
+     * @param node YamlNode, never null.
+     * @return T returned type.
+     */
+    default T visitChildren(final YamlNode node) {
+        T result = defaultResult();
+        if(node != null) {
+            for (final YamlNode child : node.children()) {
+                if(child != null) {
+                    T childResult = child.accept(this);
+                    result = aggregateResult(result, childResult);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Default result.
+     * @return T returned type.
+     */
+    T defaultResult();
+
+    /**
+     * Aggregate results.
+     * @param aggregate Previously aggregated results.
+     * @param nextResult Next result.
+     * @return T returned type.
+     */
+    T aggregateResult(final T aggregate, final T nextResult);
+}


Test patch:
diff --git a/src/test/java/com/amihaiemil/eoyaml/BuiltYamlStreamTest.java b/src/test/java/com/amihaiemil/eoyaml/BuiltYamlStreamTest.java
index 7cc3f77..d3f8318 100644
--- a/src/test/java/com/amihaiemil/eoyaml/BuiltYamlStreamTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/BuiltYamlStreamTest.java
@@ -366,10 +366,9 @@ public final class BuiltYamlStreamTest {
 
     /**
      * BuiltYamlStreams can hold an empty stream.
-     * @throws Exception If something goes wrong.
      */
     @Test
-    public void builtEmptyStream() throws Exception {
+    public void builtEmptyStream() {
         final YamlStream mappings = Yaml.createYamlStreamBuilder().build();
         MatcherAssert.assertThat(mappings, Matchers.notNullValue());
         MatcherAssert.assertThat(mappings, Matchers.instanceOf(Stream.class));
@@ -378,7 +377,7 @@ public final class BuiltYamlStreamTest {
         );
         MatcherAssert.assertThat(
             mappings.toString(),
-            Matchers.isEmptyString()
+            Matchers.equalTo("---" + System.lineSeparator() + "...")
         );
     }
 
diff --git a/src/test/java/com/amihaiemil/eoyaml/ReadYamlMappingTest.java b/src/test/java/com/amihaiemil/eoyaml/ReadYamlMappingTest.java
index e5bff82..f4e4ada 100644
--- a/src/test/java/com/amihaiemil/eoyaml/ReadYamlMappingTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/ReadYamlMappingTest.java
@@ -856,11 +856,11 @@ public final class ReadYamlMappingTest {
      * @throws Exception if something goes wrong
      */
     @Test
-    public void printsEmptyYaml() throws Exception {
+    public void printsEmptyYaml() {
         final YamlMapping map = new ReadYamlMapping(
-            new AllYamlLines(new ArrayList<YamlLine>())
+            new AllYamlLines(new ArrayList<>())
         );
-        MatcherAssert.assertThat(map.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(map.toString(), Matchers.equalTo("{}"));
     }
 
     /**
diff --git a/src/test/java/com/amihaiemil/eoyaml/ReadYamlSequenceTest.java b/src/test/java/com/amihaiemil/eoyaml/ReadYamlSequenceTest.java
index 49680c0..7592130 100644
--- a/src/test/java/com/amihaiemil/eoyaml/ReadYamlSequenceTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/ReadYamlSequenceTest.java
@@ -478,11 +478,11 @@ public final class ReadYamlSequenceTest {
      * @throws Exception if something goes wrong
      */
     @Test
-    public void printsEmptyYaml() throws Exception {
+    public void printsEmptyYaml() {
         final YamlSequence sequence = new ReadYamlSequence(
-            new AllYamlLines(new ArrayList<YamlLine>())
+            new AllYamlLines(new ArrayList<>())
         );
-        MatcherAssert.assertThat(sequence.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(sequence.toString(), Matchers.equalTo("[]"));
     }
 
     /**
diff --git a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
index 05f1dc4..0a089b1 100644
--- a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
@@ -100,9 +100,6 @@ public final class ReflectedYamlMappingTest {
             mapping.comment().value(),
             Matchers.equalTo("Information about a student")
         );
-        mapping.keys().forEach(
-            k -> System.out.println(k.comment().value())
-        );
         MatcherAssert.assertThat(
             mapping.value("classes").comment().value(),
             Matchers.equalTo("Classes the student is enrolled to.")
diff --git a/src/test/java/com/amihaiemil/eoyaml/RtYamlInputTest.java b/src/test/java/com/amihaiemil/eoyaml/RtYamlInputTest.java
index a5adf5a..b556495 100644
--- a/src/test/java/com/amihaiemil/eoyaml/RtYamlInputTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/RtYamlInputTest.java
@@ -450,12 +450,17 @@ public final class RtYamlInputTest {
         ).readYamlMapping();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
         MatcherAssert.assertThat(read.keys(), Matchers.emptyIterable());
-        MatcherAssert.assertThat(read.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("{}"));
 
         read = Yaml.createYamlInput("").readYamlMapping();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
         MatcherAssert.assertThat(read.keys(), Matchers.emptyIterable());
-        MatcherAssert.assertThat(read.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("{}"));
+
+        read = Yaml.createYamlInput("{}").readYamlMapping();
+        MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
+        MatcherAssert.assertThat(read.keys(), Matchers.emptyIterable());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("{}"));
     }
 
     /**
@@ -469,11 +474,15 @@ public final class RtYamlInputTest {
             "---" + newLine + "..."
         ).readYamlSequence();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
-        MatcherAssert.assertThat(read.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("[]"));
 
         read = Yaml.createYamlInput("").readYamlSequence();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
-        MatcherAssert.assertThat(read.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("[]"));
+
+        read = Yaml.createYamlInput("[]").readYamlSequence();
+        MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
+        MatcherAssert.assertThat(read.toString(), Matchers.equalTo("[]"));
     }
 
     /**
@@ -488,13 +497,13 @@ public final class RtYamlInputTest {
         ).readYamlStream();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
         MatcherAssert.assertThat(
-            read.toString(), Matchers.isEmptyString()
+            read.toString(), Matchers.equalTo("---" + newLine + "...")
         );
 
         read = Yaml.createYamlInput("").readYamlStream();
         MatcherAssert.assertThat(read.values(), Matchers.emptyIterable());
         MatcherAssert.assertThat(
-            read.toString(), Matchers.isEmptyString()
+            read.toString(), Matchers.equalTo("---" + newLine + "...")
         );
     }
 
diff --git a/src/test/java/com/amihaiemil/eoyaml/RtYamlMappingTest.java b/src/test/java/com/amihaiemil/eoyaml/RtYamlMappingTest.java
index 33f8a68..f9f0856 100644
--- a/src/test/java/com/amihaiemil/eoyaml/RtYamlMappingTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/RtYamlMappingTest.java
@@ -455,12 +455,11 @@ public final class RtYamlMappingTest {
 
     /**
      * An empty RtYamlMapping can be printed.
-     * @throws Exception if something goes wrong
      */
     @Test
-    public void printsEmptyYaml() throws Exception {
+    public void printsEmptyYaml() {
         YamlMapping yaml = Yaml.createYamlMappingBuilder().build();
-        MatcherAssert.assertThat(yaml.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(yaml.toString(), Matchers.equalTo("{}"));
     }
 
     /**
diff --git a/src/test/java/com/amihaiemil/eoyaml/RtYamlSequenceTest.java b/src/test/java/com/amihaiemil/eoyaml/RtYamlSequenceTest.java
index 157ad24..7b7132f 100644
--- a/src/test/java/com/amihaiemil/eoyaml/RtYamlSequenceTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/RtYamlSequenceTest.java
@@ -303,12 +303,11 @@ public final class RtYamlSequenceTest {
 
     /**
      * An empty YamlSequecne can be printed.
-     * @throws Exception if something goes wrong
      */
     @Test
-    public void printsEmptyYamlSequence() throws Exception {
+    public void printsEmptyYamlSequence() {
         YamlSequence seq = Yaml.createYamlSequenceBuilder().build();
-        MatcherAssert.assertThat(seq.toString(), Matchers.isEmptyString());
+        MatcherAssert.assertThat(seq.toString(), Matchers.equalTo("[]"));
     }
 
     /**