Major update for pretty print, adding interfaces for printer configuration, printer, ... and deprecated old PrettyPrinter API
diff --git a/javaparser-core-metamodel-generator/src/main/java/com/github/javaparser/generator/metamodel/MetaModelGenerator.java b/javaparser-core-metamodel-generator/src/main/java/com/github/javaparser/generator/metamodel/MetaModelGenerator.java
index ea9ad4a..cc63ba4 100644
--- a/javaparser-core-metamodel-generator/src/main/java/com/github/javaparser/generator/metamodel/MetaModelGenerator.java
+++ b/javaparser-core-metamodel-generator/src/main/java/com/github/javaparser/generator/metamodel/MetaModelGenerator.java
@@ -21,6 +21,15 @@
 
 package com.github.javaparser.generator.metamodel;
 
+import static com.github.javaparser.utils.Utils.decapitalize;
+
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
 import com.github.javaparser.ParserConfiguration;
 import com.github.javaparser.StaticJavaParser;
 import com.github.javaparser.ast.CompilationUnit;
@@ -30,19 +39,13 @@
 import com.github.javaparser.ast.body.MethodDeclaration;
 import com.github.javaparser.ast.stmt.Statement;
 import com.github.javaparser.generator.AbstractGenerator;
-import com.github.javaparser.printer.PrettyPrinter;
-import com.github.javaparser.printer.PrettyPrinterConfiguration;
+import com.github.javaparser.printer.PrettyPrintable;
+import com.github.javaparser.printer.Printable;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
 import com.github.javaparser.utils.SourceRoot;
 
-import java.lang.reflect.Field;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-
-import static com.github.javaparser.utils.Utils.decapitalize;
-
 public class MetaModelGenerator extends AbstractGenerator {
 
     static final String BASE_NODE_META_MODEL = "BaseNodeMetaModel";
@@ -196,7 +199,9 @@
                 .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW)
                 .setStoreTokens(false);
         final SourceRoot sourceRoot = new SourceRoot(root, parserConfiguration);
-        sourceRoot.setPrinter(new PrettyPrinter(new PrettyPrinterConfiguration().setEndOfLineCharacter("\n"))::print);
+        ConfigurablePrinter config = new PrinterConfiguration().addOption(ConfigOption.END_OF_LINE_CHARACTER.value("\n");
+        Printable printer = new PrettyPrintable(config);
+        sourceRoot.setPrinter(printer::print);
         StaticJavaParser.setConfiguration(parserConfiguration);
 
         new MetaModelGenerator(sourceRoot).generate();
diff --git a/javaparser-core-testing-bdd/src/test/java/com/github/javaparser/steps/CommentParsingSteps.java b/javaparser-core-testing-bdd/src/test/java/com/github/javaparser/steps/CommentParsingSteps.java
index 6c6a623..664efd9 100644
--- a/javaparser-core-testing-bdd/src/test/java/com/github/javaparser/steps/CommentParsingSteps.java
+++ b/javaparser-core-testing-bdd/src/test/java/com/github/javaparser/steps/CommentParsingSteps.java
@@ -31,8 +31,8 @@
 import com.github.javaparser.ast.stmt.ExpressionStmt;
 import com.github.javaparser.ast.type.PrimitiveType;
 import com.github.javaparser.printer.PrettyPrinter;
-import com.github.javaparser.printer.PrettyPrinterConfiguration;
 import com.github.javaparser.printer.Printable;
+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;
 
 import org.jbehave.core.annotations.Alias;
 import org.jbehave.core.annotations.Given;
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/ast/comments/CommentTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/ast/comments/CommentTest.java
index 7ae834a..8892e02 100644
--- a/javaparser-core-testing/src/test/java/com/github/javaparser/ast/comments/CommentTest.java
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/ast/comments/CommentTest.java
@@ -21,23 +21,29 @@
 
 package com.github.javaparser.ast.comments;
 
+import static com.github.javaparser.StaticJavaParser.parse;
+import static com.github.javaparser.utils.TestUtils.assertEqualsStringIgnoringEol;
+import static com.github.javaparser.utils.Utils.SYSTEM_EOL;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
 import com.github.javaparser.ast.CompilationUnit;
 import com.github.javaparser.ast.NodeList;
 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
 import com.github.javaparser.ast.body.MethodDeclaration;
 import com.github.javaparser.javadoc.Javadoc;
 import com.github.javaparser.javadoc.description.JavadocDescription;
-import com.github.javaparser.printer.PrettyPrinterConfiguration;
-import org.junit.jupiter.api.Test;
-
-import static com.github.javaparser.StaticJavaParser.parse;
-import static com.github.javaparser.utils.TestUtils.assertEqualsStringIgnoringEol;
-import static com.github.javaparser.utils.Utils.SYSTEM_EOL;
-import static org.junit.jupiter.api.Assertions.*;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.Indentation;
+import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
 
 class CommentTest {
 
-    private static final PrettyPrinterConfiguration PRETTY_PRINTER_CONFIG_TWO_INDENT = new PrettyPrinterConfiguration().setIndentSize(2);
+    private static final ConfigurablePrinter PRETTY_PRINTER_CONFIG_TWO_INDENT = new PrinterConfiguration().setIndentation(new Indentation(IndentType.SPACES, 2));
 
     @Test
     void removeOrphanComment() {
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintVisitorTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintVisitorTest.java
index d688aed..cefd5ca 100644
--- a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintVisitorTest.java
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintVisitorTest.java
@@ -43,6 +43,9 @@
 import com.github.javaparser.ast.expr.VariableDeclarationExpr;
 import com.github.javaparser.ast.stmt.Statement;
 import com.github.javaparser.ast.type.Type;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
 
 class PrettyPrintVisitorTest {
 
@@ -68,11 +71,11 @@
     }
 
     private String print(Node node) {
-        return new PrettyPrinter().print(node);
+        return new PrettyPrintable().print(node);
     }
 
-    private String print(Node node, PrettyPrinterConfiguration conf) {
-        return new PrettyPrinter(conf).print(node);
+    private String print(Node node, ConfigurablePrinter conf) {
+        return new PrettyPrintable(conf).print(node);
     }
 
 
@@ -88,7 +91,7 @@
      */
     @Test
     void printOperatorsR0(){
-        PrettyPrinterConfiguration conf1 = new PrettyPrinterConfiguration().setSpaceAroundOperators(false);
+        ConfigurablePrinter conf1 = new PrinterConfiguration().removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement1 = parseStatement("a = 1 + 1;");
         assertEquals("a=1+1;", print(statement1, conf1));
     }
@@ -125,19 +128,19 @@
      */
     @Test
     void printOperatorsR2(){
-        PrettyPrinterConfiguration conf1 = new PrettyPrinterConfiguration().setSpaceAroundOperators(false);
+        ConfigurablePrinter conf1 = new PrinterConfiguration().removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement1 = parseStatement("a = 1 + 1;");
         assertEquals("a=1+1;", print(statement1, conf1));
 
-        PrettyPrinterConfiguration conf2 = new PrettyPrinterConfiguration().setSpaceAroundOperators(false);
+        ConfigurablePrinter conf2 = new PrinterConfiguration().removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement2 = parseStatement("a=1+1;");
         assertEquals("a=1+1;", print(statement2, conf2));
 
-        PrettyPrinterConfiguration conf3 = new PrettyPrinterConfiguration().setSpaceAroundOperators(true);
+        ConfigurablePrinter conf3 = new PrinterConfiguration().addOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement3 = parseStatement("a = 1 + 1;");
         assertEquals("a = 1 + 1;", print(statement3, conf3));
 
-        PrettyPrinterConfiguration conf4 = new PrettyPrinterConfiguration().setSpaceAroundOperators(true);
+        ConfigurablePrinter conf4 = new PrinterConfiguration().addOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement4 = parseStatement("a=1+1;");
         assertEquals("a = 1 + 1;", print(statement4, conf4));
 
@@ -145,7 +148,7 @@
 
     @Test
     void printOperatorA(){
-        PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration().setSpaceAroundOperators(false);
+        ConfigurablePrinter conf = new PrinterConfiguration().removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         Statement statement6 = parseStatement("if(1>2&&1<3||1<3){}");
         assertEquals("if (1>2&&1<3||1<3) {" + SYSTEM_EOL
                 + "}", print(statement6, conf));
@@ -154,7 +157,7 @@
     @Test
     void printOperator2(){
         Expression expression = parseExpression("1+1");
-        PrettyPrinterConfiguration spaces = new PrettyPrinterConfiguration().setSpaceAroundOperators(false);
+        ConfigurablePrinter spaces = new PrinterConfiguration().removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         assertEquals("1+1", print(expression, spaces));
     }
 
@@ -231,7 +234,7 @@
     void printClassWithoutJavaDocButWithComment() {
         String code = String.format("/** javadoc */ public class A { %s// stuff%s}", SYSTEM_EOL, SYSTEM_EOL);
         CompilationUnit cu = parse(code);
-        PrettyPrinterConfiguration ignoreJavaDoc = new PrettyPrinterConfiguration().setPrintJavadoc(false);
+        ConfigurablePrinter ignoreJavaDoc = new PrinterConfiguration().removeOption(ConfigOption.PRINT_JAVADOC);
         String content = cu.toString(ignoreJavaDoc);
         assertEquals(String.format("public class A {%s    // stuff%s}%s", SYSTEM_EOL, SYSTEM_EOL, SYSTEM_EOL), content);
     }
@@ -253,7 +256,7 @@
     void printImportsOrdered() {
         String code = "import x.y.z;import a.b.c;import static b.c.d;class c {}";
         CompilationUnit cu = parse(code);
-        PrettyPrinterConfiguration orderImports = new PrettyPrinterConfiguration().setOrderImports(true);
+        ConfigurablePrinter orderImports = new PrinterConfiguration().addOption(ConfigOption.ORDER_IMPORTS);
         String content = cu.toString(orderImports);
         assertEqualsStringIgnoringEol("import static b.c.d;\n" +
                 "import a.b.c;\n" +
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintableTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintableTest.java
new file mode 100755
index 0000000..d9e212e
--- /dev/null
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrintableTest.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2019 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer;
+
+import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
+import static com.github.javaparser.ParserConfiguration.LanguageLevel.JAVA_9;
+import static com.github.javaparser.Providers.provider;
+import static com.github.javaparser.StaticJavaParser.parse;
+import static com.github.javaparser.StaticJavaParser.parseBodyDeclaration;
+import static com.github.javaparser.StaticJavaParser.parseStatement;
+import static com.github.javaparser.utils.TestUtils.assertEqualsStringIgnoringEol;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParseProblemException;
+import com.github.javaparser.ParseResult;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.expr.VariableDeclarationExpr;
+import com.github.javaparser.ast.stmt.Statement;
+import com.github.javaparser.ast.type.PrimitiveType;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.Indentation;
+import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
+
+class PrettyPrintableTest {
+    
+    private Printable getDefaultPrinter() {
+        ConfigurablePrinter configuration = new PrinterConfiguration();
+        return new PrettyPrintable(configuration);
+    }
+    
+    private Printable getDefaultPrinter(ConfigurablePrinter configuration) {
+        return new PrettyPrintable(configuration);
+    }
+
+    private String prettyPrintField(String code) {
+        CompilationUnit cu = parse(code);
+        return getDefaultPrinter().print(cu.findFirst(FieldDeclaration.class).get());
+    }
+
+    private String prettyPrintVar(String code) {
+        CompilationUnit cu = parse(code);
+        return getDefaultPrinter().print(cu.findAll(VariableDeclarationExpr.class).get(0));
+    }
+
+    @Test
+    void printingArrayFields() {
+        String code;
+        code = "class A { int a, b[]; }";
+        assertEquals("int a, b[];", prettyPrintField(code));
+
+        code = "class A { int[] a[], b[]; }";
+        assertEquals("int[][] a, b;", prettyPrintField(code));
+
+        code = "class A { int[] a[][], b; }";
+        assertEquals("int[] a[][], b;", prettyPrintField(code));
+
+        code = "class A { int[] a, b; }";
+        assertEquals("int[] a, b;", prettyPrintField(code));
+
+        code = "class A { int a[], b[]; }";
+        assertEquals("int[] a, b;", prettyPrintField(code));
+    }
+
+    @Test
+    void printingArrayVariables() {
+        String code;
+        code = "class A { void foo(){ int a, b[]; }}";
+        assertEquals("int a, b[]", prettyPrintVar(code));
+
+        code = "class A { void foo(){ int[] a[], b[]; }}";
+        assertEquals("int[][] a, b", prettyPrintVar(code));
+
+        code = "class A { void foo(){ int[] a[][], b; }}";
+        assertEquals("int[] a[][], b", prettyPrintVar(code));
+
+        code = "class A { void foo(){ int[] a, b; }}";
+        assertEquals("int[] a, b", prettyPrintVar(code));
+
+        code = "class A { void foo(){ int a[], b[]; }}";
+        assertEquals("int[] a, b", prettyPrintVar(code));
+    }
+
+    @Disabled
+    private String prettyPrintConfigurable(String code) {
+        CompilationUnit cu = parse(code);
+        return getDefaultPrinter().print(cu.findFirst(ClassOrInterfaceDeclaration.class).get().getName());
+    }
+
+    @Test
+    void printUseTestVisitor() {
+        String code;
+        code = "class A { void foo(){ int a, b[]; }}";
+        assertEquals("A", prettyPrintConfigurable(code));
+    }
+
+    @Test
+    void prettyColumnAlignParameters_enabled() {
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS);
+
+        final String EOL = configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString();
+
+        String code = "class Example { void foo(Object arg0,Object arg1){ myMethod(1, 2, 3, 5, Object.class); } }";
+        String expected = "class Example {" + EOL +
+                "" + EOL +
+                "    void foo(Object arg0, Object arg1) {" + EOL +
+                "        myMethod(1," + EOL +
+                "                 2," + EOL +
+                "                 3," + EOL +
+                "                 5," + EOL +
+                "                 Object.class);" + EOL +
+                "    }" + EOL +
+                "}" + EOL +
+                "";
+
+        assertEquals(expected, getDefaultPrinter(configuration).print(parse(code)));
+    }
+
+    @Test
+    void prettyColumnAlignParameters_disabled() {
+        
+        ConfigurablePrinter configuration = new PrinterConfiguration();
+        final String EOL = configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString();
+
+        String code = "class Example { void foo(Object arg0,Object arg1){ myMethod(1, 2, 3, 5, Object.class); } }";
+        String expected = "class Example {" + EOL +
+                "" + EOL +
+                "    void foo(Object arg0, Object arg1) {" + EOL +
+                "        myMethod(1, 2, 3, 5, Object.class);" + EOL +
+                "    }" + EOL +
+                "}" + EOL +
+                "";
+
+        assertEquals(expected, getDefaultPrinter(configuration).print(parse(code)));
+    }
+
+    @Test
+    void prettyAlignMethodCallChains_enabled() {
+        
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
+
+        final String EOL = configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString();
+
+        String code = "class Example { void foo() { IntStream.range(0, 10).filter(x -> x % 2 == 0).map(x -> x * IntStream.of(1,3,5,1).sum()).forEach(System.out::println); } }";
+        String expected = "class Example {" + EOL +
+                "" + EOL +
+                "    void foo() {" + EOL +
+                "        IntStream.range(0, 10)" + EOL +
+                "                 .filter(x -> x % 2 == 0)" + EOL +
+                "                 .map(x -> x * IntStream.of(1, 3, 5, 1)" + EOL +
+                "                                        .sum())" + EOL +
+                "                 .forEach(System.out::println);" + EOL +
+                "    }" + EOL +
+                "}" + EOL +
+                "";
+        
+        String printed = getDefaultPrinter(configuration).print(parse(code));
+        System.out.println(printed);
+
+        assertEquals(expected, printed);
+    }
+
+    @Test
+    void prettyAlignMethodCallChains_disabled() {
+        
+        ConfigurablePrinter configuration = new PrinterConfiguration();
+        final String EOL = configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString();
+
+        String code = "class Example { void foo() { IntStream.range(0, 10).filter(x -> x % 2 == 0).map(x -> x * IntStream.of(1,3,5,1).sum()).forEach(System.out::println); } }";
+        String expected = "class Example {" + EOL +
+                "" + EOL +
+                "    void foo() {" + EOL +
+                "        IntStream.range(0, 10).filter(x -> x % 2 == 0).map(x -> x * IntStream.of(1, 3, 5, 1).sum()).forEach(System.out::println);" + EOL +
+                "    }" + EOL +
+                "}" + EOL +
+                "";
+
+        assertEquals(expected, getDefaultPrinter(configuration).print(parse(code)));
+    }
+
+    @Test
+    void enumConstantsHorizontally() {
+        CompilationUnit cu = parse("enum X{A, B, C, D, E}");
+        assertEqualsStringIgnoringEol("enum X {\n\n    A, B, C, D, E\n}\n", new PrettyPrintable().print(cu));
+    }
+
+    @Test
+    void enumConstantsVertically() {
+        CompilationUnit cu = parse("enum X{A, B, C, D, E, F}");
+        assertEqualsStringIgnoringEol("enum X {\n\n    A,\n    B,\n    C,\n    D,\n    E,\n    F\n}\n", new PrettyPrintable().print(cu));
+    }
+
+    @Test
+    void printingInconsistentVariables() {
+        FieldDeclaration fieldDeclaration = parseBodyDeclaration("int a, b;").asFieldDeclaration();
+
+        assertEquals("int a, b;", fieldDeclaration.toString());
+
+        fieldDeclaration.getVariable(0).setType(PrimitiveType.doubleType());
+
+        assertEquals("??? a, b;", fieldDeclaration.toString());
+
+        fieldDeclaration.getVariable(1).setType(PrimitiveType.doubleType());
+
+        assertEquals("double a, b;", fieldDeclaration.toString());
+    }
+
+    @Test
+    void prettyAlignMethodCallChainsIndentsArgumentsWithBlocksCorrectly() {
+
+        CompilationUnit cu = parse("class Foo { void bar() { a.b.c.d.e; a.b.c().d().e(); a.b.c().d.e(); foo().bar().baz(boo().baa().bee()).bam(); foo().bar().baz(boo().baa().bee()).bam; foo().bar(Long.foo().b.bar(), bam).baz(); foo().bar().baz(foo, () -> { boo().baa().bee(); }).baz(() -> { boo().baa().bee(); }).bam(() -> { boo().baa().bee(); }); } }");
+        
+        Indentation indentation = new Indentation(IndentType.TABS_WITH_SPACE_ALIGN, 1);
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
+                .setIndentation(indentation);
+        String printed = getDefaultPrinter(configuration).print(cu);
+        
+        assertEqualsStringIgnoringEol("class Foo {\n" +
+                "\n" +
+                "\tvoid bar() {\n" +
+                "\t\ta.b.c.d.e;\n" +
+                "\t\ta.b.c()\n" +
+                "\t\t   .d()\n" +
+                "\t\t   .e();\n" +
+                "\t\ta.b.c().d\n" +
+                "\t\t   .e();\n" +
+                "\t\tfoo().bar()\n" +
+                "\t\t     .baz(boo().baa().bee())\n" +
+                "\t\t     .bam();\n" +
+                "\t\tfoo().bar()\n" +
+                "\t\t     .baz(boo().baa().bee()).bam;\n" +
+                "\t\tfoo().bar(Long.foo().b.bar(),\n" +
+                "\t\t          bam)\n" +
+                "\t\t     .baz();\n" +
+                "\t\tfoo().bar()\n" +
+                "\t\t     .baz(foo,\n" +
+                "\t\t          () -> {\n" +
+                "\t\t          \tboo().baa()\n" +
+                "\t\t          \t     .bee();\n" +
+                "\t\t          })\n" +
+                "\t\t     .baz(() -> {\n" +
+                "\t\t     \tboo().baa()\n" +
+                "\t\t     \t     .bee();\n" +
+                "\t\t     })\n" +
+                "\t\t     .bam(() -> {\n" +
+                "\t\t     \tboo().baa()\n" +
+                "\t\t     \t     .bee();\n" +
+                "\t\t     });\n" +
+                "\t}\n" +
+                "}\n", printed);
+    }
+
+    @Test
+    void noChainsIndentsInIf() {
+        Statement cu = parseStatement("if (x.y().z()) { boo().baa().bee(); }");
+
+        ConfigurablePrinter configuration = new PrinterConfiguration().addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
+        String printed = getDefaultPrinter(configuration).print(cu);
+
+        assertEqualsStringIgnoringEol("if (x.y().z()) {\n" +
+                "    boo().baa()\n" +
+                "         .bee();\n" +
+                "}", printed);
+    }
+
+    @Test
+    void noChainsIndentsInFor() {
+        Statement cu = parseStatement("for(int x=1; x.y().z(); x.z().z()) { boo().baa().bee(); }");
+
+        ConfigurablePrinter configuration = new PrinterConfiguration().addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
+        String printed = getDefaultPrinter(configuration).print(cu);
+
+        assertEqualsStringIgnoringEol("for (int x = 1; x.y().z(); x.z().z()) {\n" +
+                "    boo().baa()\n" +
+                "         .bee();\n" +
+                "}", printed);
+    }
+
+    @Test
+    void noChainsIndentsInWhile() {
+        Statement cu = parseStatement("while(x.y().z()) { boo().baa().bee(); }");
+
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
+        String printed = getDefaultPrinter(configuration).print(cu);
+        
+        assertEqualsStringIgnoringEol("while (x.y().z()) {\n" +
+                "    boo().baa()\n" +
+                "         .bee();\n" +
+                "}", printed);
+    }
+
+    @Test
+    void indentWithTabsAsFarAsPossible() {
+
+        CompilationUnit cu = parse("class Foo { void bar() { foo().bar().baz(() -> { boo().baa().bee(a, b, c); }).bam(); } }");
+        
+       Indentation indentation = new Indentation(IndentType.TABS, 1);
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
+                .setIndentation(indentation);
+        
+        String printed = getDefaultPrinter(configuration).print(cu);
+        
+        assertEqualsStringIgnoringEol("class Foo {\n" +
+                "\n" +
+                "\tvoid bar() {\n" +
+                "\t\tfoo().bar()\n" +
+                "\t\t\t .baz(() -> {\n" +
+                "\t\t\t\t boo().baa()\n" +
+                "\t\t\t\t\t  .bee(a,\n" +
+                "\t\t\t\t\t\t   b,\n" +
+                "\t\t\t\t\t\t   c);\n" +
+                "\t\t\t })\n" +
+                "\t\t\t .bam();\n" +
+                "\t}\n" +
+                "}\n", printed);
+    }
+
+    @Test
+    void indentWithTabsAlignWithSpaces() {
+
+        CompilationUnit cu = parse("class Foo { void bar() { foo().bar().baz(() -> { boo().baa().bee(a, b, c); }).baz(() -> { return boo().baa(); }).bam(); } }");
+        
+        Indentation indentation = new Indentation(IndentType.TABS_WITH_SPACE_ALIGN, 1);
+        ConfigurablePrinter configuration = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
+                .setIndentation(indentation);
+        
+        String printed = getDefaultPrinter(configuration).print(cu);
+
+        assertEqualsStringIgnoringEol("class Foo {\n" +
+                "\n" +
+                "\tvoid bar() {\n" +
+                "\t\tfoo().bar()\n" +
+                "\t\t     .baz(() -> {\n" +
+                "\t\t     \tboo().baa()\n" +
+                "\t\t     \t     .bee(a,\n" +
+                "\t\t     \t          b,\n" +
+                "\t\t     \t          c);\n" +
+                "\t\t     })\n" +
+                "\t\t     .baz(() -> {\n" +
+                "\t\t     \treturn boo().baa();\n" +
+                "\t\t     })\n" +
+                "\t\t     .bam();\n" +
+                "\t}\n" +
+                "}\n", printed);
+    }
+
+    @Test
+    void printAnnotationsAtPrettyPlaces() {
+
+        JavaParser javaParser = new JavaParser(new ParserConfiguration().setLanguageLevel(JAVA_9));
+        ParseResult<CompilationUnit> parseResult = javaParser.parse(COMPILATION_UNIT, provider("@Documented\n" +
+                "@Repeatable\n" +
+                "package com.github.javaparser;\n" +
+                "\n" +
+                "import java.lang.annotation.Documented;\n" +
+                "import java.lang.annotation.Repeatable;\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "@interface Annotation {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    String value();\n" +
+                "}\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "class Class<@Documented @Repeatable T> {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    byte b;\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    Class(@Documented @Repeatable int i) {\n" +
+                "        @Documented\n" +
+                "        @Repeatable\n" +
+                "        short s;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    void method(@Documented @Repeatable Class this) {\n" +
+                "        for (@Deprecated int i : arr4[0]) {\n" +
+                "            x--;\n" +
+                "        }\n" +
+                "    }\n" +
+                "\n" +
+                "    void method(@Documented @Repeatable Class this, int i) {\n" +
+                "    }\n" +
+                "}\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "enum Foo {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    BAR\n" +
+                "}\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "module foo.bar {\n" +
+                "}\n"));
+        if (!parseResult.isSuccessful()) {
+            throw new ParseProblemException(parseResult.getProblems());
+        }
+        CompilationUnit cu = parseResult.getResult().orElseThrow(AssertionError::new);
+        String printed = getDefaultPrinter().print(cu);
+
+        assertEqualsStringIgnoringEol("@Documented\n" +
+                "@Repeatable\n" +
+                "package com.github.javaparser;\n" +
+                "\n" +
+                "import java.lang.annotation.Documented;\n" +
+                "import java.lang.annotation.Repeatable;\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "@interface Annotation {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    String value();\n" +
+                "}\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "class Class<@Documented @Repeatable T> {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    byte b;\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    Class(@Documented @Repeatable int i) {\n" +
+                "        @Documented\n" +
+                "        @Repeatable\n" +
+                "        short s;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    void method(@Documented @Repeatable Class this) {\n" +
+                "        for (@Deprecated int i : arr4[0]) {\n" +
+                "            x--;\n" +
+                "        }\n" +
+                "    }\n" +
+                "\n" +
+                "    void method(@Documented @Repeatable Class this, int i) {\n" +
+                "    }\n" +
+                "}\n" +
+                "\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "enum Foo {\n" +
+                "\n" +
+                "    @Documented\n" +
+                "    @Repeatable\n" +
+                "    BAR\n" +
+                "}\n" +
+                "@Documented\n" +
+                "@Repeatable\n" +
+                "module foo.bar {\n" +
+                "}\n", printed);
+    }
+    
+    @Test
+    public void testIssue2578() {
+        String code = 
+                "class C{\n" +
+                "  //orphan\n" +
+                "  /*orphan*/\n" +
+                "}";
+        CompilationUnit cu = StaticJavaParser.parse(code);
+        TypeDeclaration td = cu.findFirst(TypeDeclaration.class).get();
+        assertEquals(2, td.getAllContainedComments().size());
+        td.setPublic(true); // --- simple AST change -----
+        System.out.println(cu.toString()); // orphan and /*orphan*/ must be printed
+        assertEquals(2, td.getAllContainedComments().size()); // the orphaned comments exist
+    }
+    
+    @Test
+    public void testIssue2535() {
+
+        String code = 
+                "public class A {\n" +
+                " public static A m() {\n" +
+                "  System.out.println(\"\");\n" +
+                "  // TODO\n" +
+                "  /* TODO */\n" +
+                "  /** TODO */\n" +
+                " }\n" +
+                "}";
+
+        StaticJavaParser.setConfiguration(new ParserConfiguration());
+
+        CompilationUnit cu = StaticJavaParser.parse(code);
+
+        // default indent is 4 spaces
+        assertTrue(cu.toString().contains("        // TODO"));
+        assertTrue(cu.toString().contains("        /* TODO */"));
+
+    }
+    
+    @Test
+    public void testIndentationWithDefaultSize() {
+        Indentation indentation = new Indentation(IndentType.SPACES);
+        assertTrue(indentation.getSize()==4);
+        assertEquals("    ", indentation.getIndent());
+        // on-the-fly modification
+        indentation.setSize(2);
+        assertTrue(indentation.getSize()==2);
+        assertEquals("  ", indentation.getIndent());
+    }
+    
+    @Test
+    public void testIndentationWithCustomSize() {
+        Indentation indentation = new Indentation(IndentType.TABS,2);
+        assertTrue(indentation.getSize()==2);
+        assertEquals("\t\t", indentation.getIndent());
+    }
+    
+    @Test
+    public void testIndentationWithOnTheFlyModifcation() {
+        Indentation indentation = new Indentation(IndentType.SPACES);
+        // on-the-fly modification
+        indentation.setSize(2);
+        assertTrue(indentation.getSize()==2);
+        assertEquals("  ", indentation.getIndent());
+        indentation.setType(IndentType.TABS);
+        assertTrue(indentation.getType() == IndentType.TABS);
+        assertEquals("\t\t", indentation.getIndent());
+    }
+}
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrinterTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrinterTest.java
index a772f79..5e75591 100644
--- a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrinterTest.java
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrettyPrinterTest.java
@@ -33,6 +33,8 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.function.Function;
+
 import org.junit.jupiter.api.Test;
 
 import com.github.javaparser.JavaParser;
@@ -47,19 +49,23 @@
 import com.github.javaparser.ast.expr.VariableDeclarationExpr;
 import com.github.javaparser.ast.stmt.Statement;
 import com.github.javaparser.ast.type.PrimitiveType;
+import com.github.javaparser.ast.visitor.VoidVisitor;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
 import com.github.javaparser.printer.configuration.Indentation;
 import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
 
 class PrettyPrinterTest {
 
     private String prettyPrintField(String code) {
         CompilationUnit cu = parse(code);
-        return new PrettyPrinter().print(cu.findFirst(FieldDeclaration.class).get());
+        return new PrettyPrintable().print(cu.findFirst(FieldDeclaration.class).get());
     }
 
     private String prettyPrintVar(String code) {
         CompilationUnit cu = parse(code);
-        return new PrettyPrinter().print(cu.findAll(VariableDeclarationExpr.class).get(0));
+        return new PrettyPrintable().print(cu.findAll(VariableDeclarationExpr.class).get(0));
     }
 
     @Test
@@ -102,7 +108,9 @@
 
     private String prettyPrintConfigurable(String code) {
         CompilationUnit cu = parse(code);
-        Printable printer = new PrettyPrinter(new PrettyPrinterConfiguration().setVisitorFactory(TestVisitor::new));
+        ConfigurablePrinter configuration = new PrinterConfiguration();
+        Function<ConfigurablePrinter, VoidVisitor<Void>> visitorFactory = (config) -> new TestVisitor(config, new DefaultPrintableSource(config));
+        Printable printer = new PrettyPrintable(visitorFactory, configuration);
         return printer.print(cu.findFirst(ClassOrInterfaceDeclaration.class).get());
     }
 
@@ -115,11 +123,9 @@
 
     @Test
     void prettyColumnAlignParameters_enabled() {
-        PrettyPrinterConfiguration config = new PrettyPrinterConfiguration()
-                .setColumnAlignParameters(true);
-
-        final String EOL = config.getEndOfLineCharacter();
-
+        ConfigurablePrinter config = new PrinterConfiguration().addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS);
+        final String EOL = config.get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue();
+        
         String code = "class Example { void foo(Object arg0,Object arg1){ myMethod(1, 2, 3, 5, Object.class); } }";
         String expected = "class Example {" + EOL +
                 "" + EOL +
@@ -133,13 +139,14 @@
                 "}" + EOL +
                 "";
 
-        assertEquals(expected, new PrettyPrinter(config).print(parse(code)));
+        assertEquals(expected, new PrettyPrintable(config).print(parse(code)));
     }
 
     @Test
     void prettyColumnAlignParameters_disabled() {
-        PrettyPrinterConfiguration config = new PrettyPrinterConfiguration();
-        final String EOL = config.getEndOfLineCharacter();
+        
+        ConfigurablePrinter config = new PrinterConfiguration();
+        final String EOL = config.get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue();
 
         String code = "class Example { void foo(Object arg0,Object arg1){ myMethod(1, 2, 3, 5, Object.class); } }";
         String expected = "class Example {" + EOL +
@@ -150,15 +157,14 @@
                 "}" + EOL +
                 "";
 
-        assertEquals(expected, new PrettyPrinter(config).print(parse(code)));
+        assertEquals(expected, new PrettyPrintable(config).print(parse(code)));
     }
 
     @Test
     void prettyAlignMethodCallChains_enabled() {
-        PrettyPrinterConfiguration config = new PrettyPrinterConfiguration()
-                .setColumnAlignFirstMethodChain(true);
-
-        final String EOL = config.getEndOfLineCharacter();
+        
+        ConfigurablePrinter config = new PrinterConfiguration().addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
+        final String EOL = config.get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue();
 
         String code = "class Example { void foo() { IntStream.range(0, 10).filter(x -> x % 2 == 0).map(x -> x * IntStream.of(1,3,5,1).sum()).forEach(System.out::println); } }";
         String expected = "class Example {" + EOL +
@@ -173,13 +179,14 @@
                 "}" + EOL +
                 "";
 
-        assertEquals(expected, new PrettyPrinter(config).print(parse(code)));
+        assertEquals(expected, new PrettyPrintable(config).print(parse(code)));
     }
 
     @Test
     void prettyAlignMethodCallChains_disabled() {
-        PrettyPrinterConfiguration config = new PrettyPrinterConfiguration();
-        final String EOL = config.getEndOfLineCharacter();
+        
+        ConfigurablePrinter config = new PrinterConfiguration();
+        final String EOL = config.get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue();
 
         String code = "class Example { void foo() { IntStream.range(0, 10).filter(x -> x % 2 == 0).map(x -> x * IntStream.of(1,3,5,1).sum()).forEach(System.out::println); } }";
         String expected = "class Example {" + EOL +
@@ -190,19 +197,21 @@
                 "}" + EOL +
                 "";
 
-        assertEquals(expected, new PrettyPrinter(config).print(parse(code)));
+        String printed = new PrettyPrintable(config).print(parse(code));
+        
+        assertEquals(expected, printed);
     }
 
     @Test
     void enumConstantsHorizontally() {
         CompilationUnit cu = parse("enum X{A, B, C, D, E}");
-        assertEqualsStringIgnoringEol("enum X {\n\n    A, B, C, D, E\n}\n", new PrettyPrinter().print(cu));
+        assertEqualsStringIgnoringEol("enum X {\n\n    A, B, C, D, E\n}\n", new PrettyPrintable().print(cu));
     }
 
     @Test
     void enumConstantsVertically() {
         CompilationUnit cu = parse("enum X{A, B, C, D, E, F}");
-        assertEqualsStringIgnoringEol("enum X {\n\n    A,\n    B,\n    C,\n    D,\n    E,\n    F\n}\n", new PrettyPrinter().print(cu));
+        assertEqualsStringIgnoringEol("enum X {\n\n    A,\n    B,\n    C,\n    D,\n    E,\n    F\n}\n", new PrettyPrintable().print(cu));
     }
 
     @Test
@@ -225,9 +234,14 @@
 
         CompilationUnit cu = parse("class Foo { void bar() { a.b.c.d.e; a.b.c().d().e(); a.b.c().d.e(); foo().bar().baz(boo().baa().bee()).bam(); foo().bar().baz(boo().baa().bee()).bam; foo().bar(Long.foo().b.bar(), bam).baz(); foo().bar().baz(foo, () -> { boo().baa().bee(); }).baz(() -> { boo().baa().bee(); }).bam(() -> { boo().baa().bee(); }); } }");
         Indentation indentation = new Indentation(TABS_WITH_SPACE_ALIGN, 1);
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration().setColumnAlignFirstMethodChain(true).setColumnAlignParameters(true).setIndentation(indentation))
-                .print(cu);
 
+        ConfigurablePrinter config = new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
+                .setIndentation(indentation);
+        
+        String printed = new PrettyPrintable(config).print(cu);
+        
         assertEqualsStringIgnoringEol("class Foo {\n" +
                 "\n" +
                 "\tvoid bar() {\n" +
@@ -267,7 +281,8 @@
     void noChainsIndentsInIf() {
         Statement cu = parseStatement("if (x.y().z()) { boo().baa().bee(); }");
 
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration().setColumnAlignFirstMethodChain(true))
+        String printed = new PrettyPrintable(new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN))
                 .print(cu);
 
         assertEqualsStringIgnoringEol("if (x.y().z()) {\n" +
@@ -280,7 +295,8 @@
     void noChainsIndentsInFor() {
         Statement cu = parseStatement("for(int x=1; x.y().z(); x.z().z()) { boo().baa().bee(); }");
 
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration().setColumnAlignFirstMethodChain(true))
+        String printed = new PrettyPrintable(new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN))
                 .print(cu);
 
         assertEqualsStringIgnoringEol("for (int x = 1; x.y().z(); x.z().z()) {\n" +
@@ -293,7 +309,8 @@
     void noChainsIndentsInWhile() {
         Statement cu = parseStatement("while(x.y().z()) { boo().baa().bee(); }");
 
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration().setColumnAlignFirstMethodChain(true))
+        String printed = new PrettyPrintable(new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN))
                 .print(cu);
 
         assertEqualsStringIgnoringEol("while (x.y().z()) {\n" +
@@ -307,9 +324,9 @@
 
         CompilationUnit cu = parse("class Foo { void bar() { foo().bar().baz(() -> { boo().baa().bee(a, b, c); }).bam(); } }");
         Indentation indentation = new Indentation(TABS, 1);
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration()
-                .setColumnAlignFirstMethodChain(true)
-                .setColumnAlignParameters(true)
+        String printed = new PrettyPrintable(new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
                 .setIndentation(indentation))
                 .print(cu);
 
@@ -333,9 +350,9 @@
 
         CompilationUnit cu = parse("class Foo { void bar() { foo().bar().baz(() -> { boo().baa().bee(a, b, c); }).baz(() -> { return boo().baa(); }).bam(); } }");
         Indentation indentation = new Indentation(TABS_WITH_SPACE_ALIGN, 1);
-        String printed = new PrettyPrinter(new PrettyPrinterConfiguration()
-                .setColumnAlignFirstMethodChain(true)
-                .setColumnAlignParameters(true)
+        String printed = new PrettyPrintable(new PrinterConfiguration()
+                .addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN)
+                .addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS)
                 .setIndentation(indentation))
                 .print(cu);
 
@@ -421,7 +438,7 @@
             throw new ParseProblemException(parseResult.getProblems());
         }
         CompilationUnit cu = parseResult.getResult().orElseThrow(AssertionError::new);
-        String printed = new PrettyPrinter().print(cu);
+        String printed = new PrettyPrintable().print(cu);
 
         assertEqualsStringIgnoringEol("@Documented\n" +
                 "@Repeatable\n" +
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrinterConfigurationTest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrinterConfigurationTest.java
new file mode 100755
index 0000000..97e7501
--- /dev/null
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/PrinterConfigurationTest.java
@@ -0,0 +1,67 @@
+package com.github.javaparser.printer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
+import com.github.javaparser.utils.Utils;
+
+class PrinterConfigurationTest {
+
+    @Test
+    void testDefaultConfigurationAndValue() {
+        ConfigurablePrinter config = new PrinterConfiguration();
+        assertTrue(config.get(ConfigOption.PRINT_COMMENTS).isPresent());
+        assertTrue(config.get(ConfigOption.PRINT_JAVADOC).isPresent());
+        assertTrue(config.get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent());
+        assertTrue(config.get(ConfigOption.INDENT_CASE_IN_SWITCH).isPresent());
+        assertTrue(config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).isPresent());
+        assertTrue(config.get(ConfigOption.END_OF_LINE_CHARACTER).isPresent());
+        // values
+        assertEquals(config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asValue(),
+                Integer.valueOf(5));
+        assertEquals(config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asValue(),
+                Integer.valueOf(5));
+        assertTrue(config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asValue() ==
+                Integer.valueOf(5));
+        assertEquals(config.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString(), Utils.SYSTEM_EOL);
+    }
+
+    @Test
+    void testConfigurationError() {
+        ConfigurablePrinter config = new PrinterConfiguration();
+        // verify configuration error case
+        assertThrows(IllegalArgumentException.class, () -> {
+            config.get(ConfigOption.PRINT_COMMENTS).get().asValue();
+        });
+    }
+    
+    @Test
+    void testUpdatedConfigurationOption() {
+        ConfigurablePrinter config = new PrinterConfiguration();
+        // change the default value of the DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY option
+        config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().value(2);
+        // verify the value is updated
+        assertEquals(config.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asValue(), Integer.valueOf(2));
+    }
+    
+    @Test
+    void testRemoveOption() {
+        ConfigurablePrinter config = new PrinterConfiguration();
+        assertTrue(config.get(ConfigOption.PRINT_COMMENTS).isPresent());
+        assertTrue(config.get(ConfigOption.END_OF_LINE_CHARACTER).isPresent());
+        // remove option PRINT_COMMENTS
+        config.removeOption(ConfigOption.PRINT_COMMENTS);
+        assertFalse(config.get(ConfigOption.PRINT_COMMENTS).isPresent());
+        // remove option with value
+        config.removeOption(ConfigOption.END_OF_LINE_CHARACTER.value("\n"));
+        assertFalse(config.get(ConfigOption.END_OF_LINE_CHARACTER).isPresent());
+    }
+
+}
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/TestVisitor.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/TestVisitor.java
index cf3e557..65fcdbc 100644
--- a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/TestVisitor.java
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/TestVisitor.java
@@ -22,13 +22,14 @@
 package com.github.javaparser.printer;
 
 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
 
-public class TestVisitor extends PrettyPrintVisitor {
+public class TestVisitor extends PrintableVisitor {
 
-    public TestVisitor(PrettyPrinterConfiguration prettyPrinterConfiguration) {
-        super(prettyPrinterConfiguration);
+    public TestVisitor(ConfigurablePrinter configuration, PrintableSource printer) {
+        super(configuration, printer);
     }
-
+    
     @Override
     public void visit(final ClassOrInterfaceDeclaration n, final Void arg) {
         printer.print("test");
diff --git a/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/AATest.java b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/AATest.java
new file mode 100755
index 0000000..257d137
--- /dev/null
+++ b/javaparser-core-testing/src/test/java/com/github/javaparser/printer/lexicalpreservation/AATest.java
@@ -0,0 +1,51 @@
+package com.github.javaparser.printer.lexicalpreservation;

+

+/*

+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.

+ * Copyright (C) 2011, 2013-2019 The JavaParser Team.

+ *

+ * This file is part of JavaParser.

+ *

+ * JavaParser can be used either under the terms of

+ * a) the GNU Lesser General Public License as published by

+ *     the Free Software Foundation, either version 3 of the License, or

+ *     (at your option) any later version.

+ * b) the terms of the Apache License

+ *

+ * You should have received a copy of both licenses in LICENCE.LGPL and

+ * LICENCE.APACHE. Please refer to those files for details.

+ *

+ * JavaParser is distributed in the hope that it will be useful,

+ * but WITHOUT ANY WARRANTY; without even the implied warranty of

+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

+ * GNU Lesser General Public License for more details.

+ */

+

+import org.junit.jupiter.api.Test;

+

+import com.github.javaparser.ParserConfiguration;

+import com.github.javaparser.StaticJavaParser;

+import com.github.javaparser.ast.CompilationUnit;

+import com.github.javaparser.ast.ImportDeclaration;

+import com.github.javaparser.ast.NodeList;

+

+public class AATest  extends AbstractLexicalPreservingTest {

+

+    @Test

+    public void test() {

+        String actual = 

+                "package com.wangym.test;\nclass A{ }";

+        

+        StaticJavaParser.setConfiguration(new ParserConfiguration());

+      

+        CompilationUnit cu = StaticJavaParser.parse(actual);

+        LexicalPreservingPrinter.setup(cu);

+        

+        NodeList<ImportDeclaration> imports = cu.getImports();

+        String str = "lombok.Data";

+        imports.add(new ImportDeclaration(str, false, false));

+        

+        System.out.println(LexicalPreservingPrinter.print(cu));

+    }

+    

+}

diff --git a/javaparser-core/src/main/java/com/github/javaparser/ast/CompilationUnit.java b/javaparser-core/src/main/java/com/github/javaparser/ast/CompilationUnit.java
index 7522e35..fd32255 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/ast/CompilationUnit.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/ast/CompilationUnit.java
@@ -29,6 +29,7 @@
 import static com.github.javaparser.ast.Modifier.createModifierList;
 import static com.github.javaparser.utils.CodeGenerationUtils.subtractPaths;
 import static com.github.javaparser.utils.Utils.assertNotNull;
+
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
@@ -39,6 +40,7 @@
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+
 import com.github.javaparser.JavaParser;
 import com.github.javaparser.JavaToken;
 import com.github.javaparser.ParseResult;
@@ -62,7 +64,6 @@
 import com.github.javaparser.metamodel.InternalProperty;
 import com.github.javaparser.metamodel.JavaParserMetaModel;
 import com.github.javaparser.metamodel.OptionalProperty;
-import com.github.javaparser.printer.PrettyPrinter;
 import com.github.javaparser.printer.Printable;
 import com.github.javaparser.utils.ClassUtils;
 import com.github.javaparser.utils.CodeGenerationUtils;
@@ -99,6 +100,9 @@
 
     @InternalProperty
     private Storage storage;
+    
+    // printer used to print the node
+    private Printable printer;
 
     public CompilationUnit() {
         this(null, null, new NodeList<>(), new NodeList<>(), null);
@@ -137,6 +141,25 @@
     public <A> void accept(final VoidVisitor<A> v, final A arg) {
         v.visit(this, arg);
     }
+    
+    /**
+     * Declare a specific printer
+     */
+    public CompilationUnit printer(Printable printer) {
+        this.printer = printer;
+        return this;
+    }
+    
+    /*
+     * If there is no declared printer, returns a new default printer else returns a new printer with the current configuration
+     */
+    @Override
+    public Printable getPrinter() {
+        if (printer == null) {
+            printer = createDefaultPrinter();
+        }
+        return printer;
+    }
 
     /**
      * @deprecated getComments was a too generic name and it could be confused with getComment
@@ -684,8 +707,6 @@
 
         private final Charset encoding;
 
-        private Printable printer;
-
         private Storage(CompilationUnit compilationUnit, Path path) {
             this(compilationUnit, path, UTF8);
         }
@@ -694,22 +715,6 @@
             this.compilationUnit = compilationUnit;
             this.path = path.toAbsolutePath();
             this.encoding = encoding;
-            // default printer
-            this.printer = new PrettyPrinter();
-        }
-
-        /**
-         * Set a new printer
-         */
-        public void setPrinter(Printable printer) {
-            this.printer = printer;
-        }
-
-        /**
-         * Returns the internal printer
-         */
-        public Printable getPrinter() {
-            return this.printer;
         }
 
         /**
@@ -755,7 +760,7 @@
          * Saves the compilation unit to its original location
          */
         public void save() {
-            save(cu -> printer.print(cu));
+            save(cu -> compilationUnit.getPrinter().print(cu));
         }
 
         /**
diff --git a/javaparser-core/src/main/java/com/github/javaparser/ast/Node.java b/javaparser-core/src/main/java/com/github/javaparser/ast/Node.java
index f4db8ed..9741f76 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/ast/Node.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/ast/Node.java
@@ -20,13 +20,13 @@
  */
 package com.github.javaparser.ast;
 
-import com.github.javaparser.HasParentNode;
 import static com.github.javaparser.ast.Node.Parsedness.PARSED;
 import static com.github.javaparser.ast.Node.TreeTraversal.PREORDER;
 import static java.util.Collections.emptySet;
 import static java.util.Collections.unmodifiableList;
 import static java.util.Spliterator.DISTINCT;
 import static java.util.Spliterator.NONNULL;
+
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashSet;
@@ -44,6 +44,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+
+import com.github.javaparser.HasParentNode;
 import com.github.javaparser.Position;
 import com.github.javaparser.Range;
 import com.github.javaparser.TokenRange;
@@ -64,8 +66,11 @@
 import com.github.javaparser.metamodel.NodeMetaModel;
 import com.github.javaparser.metamodel.OptionalProperty;
 import com.github.javaparser.metamodel.PropertyMetaModel;
-import com.github.javaparser.printer.PrettyPrinter;
-import com.github.javaparser.printer.PrettyPrinterConfiguration;
+import com.github.javaparser.printer.PrettyPrintable;
+import com.github.javaparser.printer.Printable;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
 import com.github.javaparser.resolution.SymbolResolver;
 import com.github.javaparser.utils.LineSeparator;
 
@@ -157,10 +162,8 @@
         return 0;
     };
 
-    private static PrettyPrinterConfiguration toStringPrettyPrinterConfiguration = new PrettyPrinterConfiguration();
-
-    protected static final PrettyPrinterConfiguration prettyPrinterNoCommentsConfiguration = new PrettyPrinterConfiguration().setPrintComments(false);
-
+    protected static final ConfigurablePrinter prettyPrinterNoCommentsConfiguration = new PrinterConfiguration().removeOption(ConfigOption.PRINT_COMMENTS);
+    
     @InternalProperty
     private Range range;
 
@@ -199,6 +202,37 @@
      */
     protected void customInitialization() {
     }
+    
+    /*
+     * If there is a printer defined in CompilationUnit, returns it
+     * else create a new PrettyPrintable with default parameters
+     */
+    protected Printable getPrinter() {
+        Optional<CompilationUnit> cu = findCompilationUnit();
+        if (!cu.isPresent()) {
+            return createDefaultPrinter();
+        }
+        return cu.get().getPrinter();
+    }
+    
+    /*
+     * Return the printer initialized with the specified configuration
+     */
+    protected Printable getPrinter(ConfigurablePrinter configuration) {
+        return getPrinter().setConfiguration(configuration);
+    }
+    
+    protected Printable createDefaultPrinter() {
+        ConfigurablePrinter configuration = getDefaultPrinterConfiguration();
+        return new PrettyPrintable(configuration);
+    }
+    
+    /*
+     * returns a default printer configuration
+     */
+    protected  ConfigurablePrinter getDefaultPrinterConfiguration() {
+        return new PrinterConfiguration();
+    }
 
     /**
      * This is a comment associated with this node.
@@ -287,23 +321,24 @@
 
     /**
      * @return pretty printed source code for this node and its children.
-     * Formatting can be configured with Node.setToStringPrettyPrinterConfiguration.
      */
     @Override
     public final String toString() {
         if (containsData(LINE_SEPARATOR_KEY)) {
             LineSeparator lineSeparator = getLineEndingStyleOrDefault(LineSeparator.SYSTEM);
-            toStringPrettyPrinterConfiguration.setEndOfLineCharacter(lineSeparator.asRawString());
+            ConfigurablePrinter config = getDefaultPrinterConfiguration();
+            config.addOption(ConfigOption.END_OF_LINE_CHARACTER.value(lineSeparator.asRawString()));
+            return getPrinter(config).print(this);
         }
-        return new PrettyPrinter(toStringPrettyPrinterConfiguration).print(this);
+        return getPrinter().print(this);
     }
 
     /**
      * @return pretty printed source code for this node and its children.
-     * Formatting can be configured with parameter prettyPrinterConfiguration.
+     * Formatting can be configured with parameter ConfigurablePrinter.
      */
-    public final String toString(PrettyPrinterConfiguration prettyPrinterConfiguration) {
-        return new PrettyPrinter(prettyPrinterConfiguration).print(this);
+    public final String toString(ConfigurablePrinter configuration) {
+        return getPrinter(configuration).print(this);
     }
 
     @Override
@@ -690,14 +725,6 @@
         return this;
     }
 
-    public static PrettyPrinterConfiguration getToStringPrettyPrinterConfiguration() {
-        return toStringPrettyPrinterConfiguration;
-    }
-
-    public static void setToStringPrettyPrinterConfiguration(PrettyPrinterConfiguration toStringPrettyPrinterConfiguration) {
-        Node.toStringPrettyPrinterConfiguration = toStringPrettyPrinterConfiguration;
-    }
-
     @Generated("com.github.javaparser.generator.core.node.ReplaceMethodGenerator")
     public boolean replace(Node node, Node replacementNode) {
         if (node == null)
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/DefaultPrintableSource.java b/javaparser-core/src/main/java/com/github/javaparser/printer/DefaultPrintableSource.java
new file mode 100755
index 0000000..9bc6118
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/DefaultPrintableSource.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2011, 2013-2020 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer;
+
+import static com.github.javaparser.printer.configuration.Indentation.IndentType.SPACES;
+
+import java.util.Deque;
+import java.util.LinkedList;
+
+import com.github.javaparser.Position;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.Indentation;
+import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
+import com.github.javaparser.utils.Utils;
+
+/**
+ * A support class for code that outputs formatted source code.
+ */
+public class DefaultPrintableSource implements PrintableSource {
+    private final String endOfLineCharacter;
+    private final Indentation indentation;
+
+    private final Deque<String> indents = new LinkedList<>();
+    private final Deque<String> reindentedIndents = new LinkedList<>();
+    private String lastPrintedIndent = "";
+    private StringBuilder buf = new StringBuilder();
+    private Position cursor = new Position(Position.FIRST_LINE, Position.FIRST_COLUMN - 1); // Start before the first column
+    private boolean indented = false;
+
+    public DefaultPrintableSource(final ConfigurablePrinter configuration) {
+        indentation = configuration.getIndentation();
+        endOfLineCharacter = configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString();
+        indents.push("");
+    }
+
+    /**
+     * Add the default indentation to the current indentation and push it on the indentation stack.
+     * Does not actually output anything.
+     */
+    @Override
+    public PrintableSource indent() {
+        String currentIndent = indents.peek();
+        switch (indentation.getType()) {
+            case SPACES:
+            case TABS_WITH_SPACE_ALIGN:
+                indents.push(currentIndent + indentation.getIndent());
+                break;
+
+            case TABS:
+                indents.push(indentation.getIndent() + currentIndent);
+                break;
+
+            default:
+                throw new AssertionError("Unhandled indent type");
+        }
+        return this;
+    }
+
+    /**
+     * Add to the current indentation until it is reaches "column" and push it on the indentation stack.
+     * Does not actually output anything.
+     */
+    @Override
+    public PrintableSource indentWithAlignTo(int column) {
+        indents.push(calculateIndentWithAlignTo(column));
+        return this;
+    }
+
+    private String calculateIndentWithAlignTo(int column) {
+        if (column < lastPrintedIndent.length()){
+            throw new IllegalStateException("Attempt to indent less than the previous indent.");
+        }
+
+        StringBuilder newIndent = new StringBuilder(lastPrintedIndent);
+        switch (indentation.getType()) {
+            case SPACES:
+            case TABS_WITH_SPACE_ALIGN:
+                while (newIndent.length() < column) {
+                    newIndent.append(SPACES.getCar());
+                }
+                break;
+
+            case TABS:
+                IndentType currentIndentType = indentation.getType(); 
+                int logicalIndentLength = newIndent.length();
+                while ((logicalIndentLength + currentIndentType.getWidth()) <= column) {
+                    newIndent.insert(0, currentIndentType.getCar());
+                    logicalIndentLength += currentIndentType.getWidth();
+                }
+                while (logicalIndentLength < column) {
+                    newIndent.append(SPACES.getCar());
+                    logicalIndentLength++;
+                }
+                StringBuilder fullTab = new StringBuilder();
+                for(int i=0; i<currentIndentType.getWidth(); i++){
+                    fullTab.append(SPACES.getCar());
+                }
+                String fullTabString = fullTab.toString();
+                if ((newIndent.length() >= currentIndentType.getWidth())
+                        && newIndent.substring(newIndent.length() - currentIndentType.getWidth()).equals(fullTabString)) {
+                    int i = newIndent.indexOf(fullTabString);
+                    newIndent.replace(i, i + currentIndentType.getWidth(), currentIndentType.getCar().toString());
+                }
+                break;
+
+            default:
+                throw new AssertionError("Unhandled indent type");
+        }
+
+        return newIndent.toString();
+    }
+
+    /**
+     * Pop the last indentation of the indentation stack.
+     * Does not actually output anything.
+     */
+    @Override
+    public PrintableSource unindent() {
+        if (indents.isEmpty()) {
+            // Since we start out with an empty indent on the stack, this will only occur
+            // the second time we over-unindent.
+            throw new IllegalStateException("Indent/unindent calls are not well-balanced.");
+        }
+        indents.pop();
+        return this;
+    }
+
+    private void append(String arg) {
+        buf.append(arg);
+        cursor = cursor.withColumn(cursor.column + arg.length());
+    }
+
+    /**
+     * Append the source string passed as argument to the buffer.
+     * If this is being appended at the beginning of a line, performs indentation first.
+     * <p>
+     * The source line to be printed should not contain newline/carriage-return characters;
+     * use {@link #println(String)} to automatically append a newline at the end of the source string.
+     * If the source line passed as argument contains newline/carriage-return characters would
+     * impredictably affect a correct computation of the current {@link #getCursor()} position.
+     *
+     * @param arg source line to be printed (should not contain newline/carriage-return characters)
+     * @return this instance, for nesting calls to method as fluent interface
+     * @see DefaultPrintableSource#println(String)
+     */
+    @Override
+    public PrintableSource print(final String arg) {
+        if (!indented) {
+            lastPrintedIndent = indents.peek();
+            append(lastPrintedIndent);
+            indented = true;
+        }
+        append(arg);
+        return this;
+    }
+
+    /**
+     * Append the source string passed as argument to the buffer, then append a newline.
+     * If this is being appended at the beginning of a line, performs indentation first.
+     * <p>
+     * The source line to be printed should not contain newline/carriage-return characters.
+     * If the source line passed as argument contains newline/carriage-return characters would
+     * impredictably affect a correct computation of the current {@link #getCursor()} position.
+     *
+     * @param arg source line to be printed (should not contain newline/carriage-return characters)
+     * @return this instance, for nesting calls to method as fluent interface
+     */
+    @Override
+    public PrintableSource println(final String arg) {
+        print(arg);
+        println();
+        return this;
+    }
+
+    /**
+     * Append a newline to the buffer.
+     *
+     * @return this instance, for nesting calls to method as fluent interface
+     */
+    @Override
+    public PrintableSource println() {
+        buf.append(endOfLineCharacter);
+        cursor = new Position(cursor.line + 1, Position.FIRST_COLUMN - 1); // Start before the first column
+        indented = false;
+        return this;
+    }
+
+    /**
+     * Return the current cursor position (line, column) in the source printer buffer.
+     * <p>
+     * Please notice in order to guarantee a correct computation of the cursor position,
+     * this printer expect the contracts of the methods {@link #print(String)} and {@link #println(String)}
+     * has been respected through all method calls, meaning the source string passed as argument to those method
+     * calls did not contain newline/carriage-return characters.
+     *
+     * @return the current cursor position (line, column).
+     */
+    @Override
+    public Position getCursor() {
+        return cursor;
+    }
+
+    /**
+     * @return the currently printed source code.
+     */
+    @Override
+    public String toString() {
+        return buf.toString();
+    }
+
+    /**
+     * Changes all EOL characters in "content" to the EOL character this SourcePrinter is using.
+     */
+    @Override
+    public String normalizeEolInTextBlock(String content) {
+        return Utils.normalizeEolInTextBlock(content, endOfLineCharacter);
+    }
+
+    /**
+     * Set the top-most indent to the column the cursor is currently in, can be undone with
+     * {@link #reindentToPreviousLevel()}. Does not actually output anything.
+     */
+    @Override
+    public void reindentWithAlignToCursor() {
+        String newIndent = calculateIndentWithAlignTo(cursor.column);
+        reindentedIndents.push(indents.pop());
+        indents.push(newIndent);
+    }
+
+    /**
+     * Set the top-most indent to the column the cursor was before the last {@link #reindentWithAlignToCursor()} call.
+     * Does not actually output anything.
+     */
+    @Override
+    public void reindentToPreviousLevel() {
+        if (reindentedIndents.isEmpty()) {
+            throw new IllegalStateException("Reindent calls are not well-balanced.");
+        }
+        indents.pop();
+        indents.push(reindentedIndents.pop());
+    }
+
+    /**
+     * Adds an indent to the top of the stack that is a copy of the current top indent.
+     * With this you announce "I'm going to indent the next line(s)" but not how far yet.
+     * Once you do know, you can pop this indent ("unindent") and indent to the right column.
+     * (Does not actually output anything.)
+     */
+    @Override
+    public void duplicateIndent() {
+        indents.push(indents.peek());
+    }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintVisitor.java b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintVisitor.java
index a9b4ec5..d179f20 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintVisitor.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintVisitor.java
@@ -62,7 +62,45 @@
 import com.github.javaparser.ast.comments.Comment;
 import com.github.javaparser.ast.comments.JavadocComment;
 import com.github.javaparser.ast.comments.LineComment;
-import com.github.javaparser.ast.expr.*;
+import com.github.javaparser.ast.expr.AnnotationExpr;
+import com.github.javaparser.ast.expr.ArrayAccessExpr;
+import com.github.javaparser.ast.expr.ArrayCreationExpr;
+import com.github.javaparser.ast.expr.ArrayInitializerExpr;
+import com.github.javaparser.ast.expr.AssignExpr;
+import com.github.javaparser.ast.expr.BinaryExpr;
+import com.github.javaparser.ast.expr.BooleanLiteralExpr;
+import com.github.javaparser.ast.expr.CastExpr;
+import com.github.javaparser.ast.expr.CharLiteralExpr;
+import com.github.javaparser.ast.expr.ClassExpr;
+import com.github.javaparser.ast.expr.ConditionalExpr;
+import com.github.javaparser.ast.expr.DoubleLiteralExpr;
+import com.github.javaparser.ast.expr.EnclosedExpr;
+import com.github.javaparser.ast.expr.Expression;
+import com.github.javaparser.ast.expr.FieldAccessExpr;
+import com.github.javaparser.ast.expr.InstanceOfExpr;
+import com.github.javaparser.ast.expr.IntegerLiteralExpr;
+import com.github.javaparser.ast.expr.LambdaExpr;
+import com.github.javaparser.ast.expr.LongLiteralExpr;
+import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
+import com.github.javaparser.ast.expr.MemberValuePair;
+import com.github.javaparser.ast.expr.MethodCallExpr;
+import com.github.javaparser.ast.expr.MethodReferenceExpr;
+import com.github.javaparser.ast.expr.Name;
+import com.github.javaparser.ast.expr.NameExpr;
+import com.github.javaparser.ast.expr.NormalAnnotationExpr;
+import com.github.javaparser.ast.expr.NullLiteralExpr;
+import com.github.javaparser.ast.expr.ObjectCreationExpr;
+import com.github.javaparser.ast.expr.PatternExpr;
+import com.github.javaparser.ast.expr.SimpleName;
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
+import com.github.javaparser.ast.expr.StringLiteralExpr;
+import com.github.javaparser.ast.expr.SuperExpr;
+import com.github.javaparser.ast.expr.SwitchExpr;
+import com.github.javaparser.ast.expr.TextBlockLiteralExpr;
+import com.github.javaparser.ast.expr.ThisExpr;
+import com.github.javaparser.ast.expr.TypeExpr;
+import com.github.javaparser.ast.expr.UnaryExpr;
+import com.github.javaparser.ast.expr.VariableDeclarationExpr;
 import com.github.javaparser.ast.modules.ModuleDeclaration;
 import com.github.javaparser.ast.modules.ModuleExportsDirective;
 import com.github.javaparser.ast.modules.ModuleOpensDirective;
@@ -112,20 +150,29 @@
 import com.github.javaparser.ast.type.WildcardType;
 import com.github.javaparser.ast.visitor.Visitable;
 import com.github.javaparser.ast.visitor.VoidVisitor;
+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;
 
 /**
  * Outputs the AST as formatted Java source code.
- *
+ * This class is no longer acceptable to use because it is not sufficiently configurable and it is too tied to a specific implementation
+ * <p> Use {@link PrintableVisitor default implementation } instead.
  * @author Julio Vilmar Gesser
+ * This class is no longer acceptable to use because it is not sufficiently configurable and it is too tied to a specific implementation
+ * <p> Use {@link PrintableVisitor default implementation } instead.
  */
+@Deprecated
 public class PrettyPrintVisitor implements VoidVisitor<Void> {
-    protected final PrettyPrinterConfiguration configuration;
+    protected PrettyPrinterConfiguration configuration;
     protected final SourcePrinter printer;
 
     public PrettyPrintVisitor(PrettyPrinterConfiguration prettyPrinterConfiguration) {
-        configuration = prettyPrinterConfiguration;
+        this.configuration = prettyPrinterConfiguration;
         printer = new SourcePrinter(configuration);
     }
+    
+    public void setConfiguration(PrettyPrinterConfiguration prettyPrinterConfiguration) {
+        this.configuration = prettyPrinterConfiguration;
+    }
 
     /**
      * @deprecated use toString()
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintable.java b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintable.java
new file mode 100755
index 0000000..fe5e75f
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrintable.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
+ * Copyright (C) 2011, 2013-2020 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer;
+
+import java.util.function.Function;
+
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.visitor.VoidVisitor;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+
+/**
+ * Pretty printer for AST nodes.
+ */
+public class PrettyPrintable implements Printable {
+    
+    private ConfigurablePrinter configuration;
+    
+    // visitor factory
+    Function<ConfigurablePrinter, VoidVisitor<Void>> visitorFactory;
+    
+    // static methods 
+    
+    private static Function<ConfigurablePrinter, VoidVisitor<Void>> createDefaultVisitor() {
+        ConfigurablePrinter configuration = createDefaultConfiguration();
+        return createDefaultVisitor(configuration);
+    }
+    
+    private static Function<ConfigurablePrinter, VoidVisitor<Void>> createDefaultVisitor(ConfigurablePrinter configuration) {
+        return (config) -> new PrintableVisitor(config, new DefaultPrintableSource(config));
+    }
+    
+    private static ConfigurablePrinter createDefaultConfiguration() {
+        return new PrinterConfiguration();
+    }
+    
+    // Constructors
+
+    /**
+     * Build a new PrettyPrintable with a default configuration and a default factory
+     */
+    public PrettyPrintable() {
+        this(createDefaultVisitor(), createDefaultConfiguration() );
+    }
+    
+    /**
+     * Build a new PrettyPrintable with a configuration and a default factory
+     * @param configuration
+     */
+    public PrettyPrintable(ConfigurablePrinter configuration) {
+        this(createDefaultVisitor(configuration), configuration );
+    }
+    
+    /**
+     * Build a new PrettyPrintable with a configuration and a factory to create a visitor to browse the nodes of the AST
+     * @param visitorFactory 
+     * @param configuration Configuration to apply
+     */
+    public PrettyPrintable(Function<ConfigurablePrinter, VoidVisitor<Void>> visitorFactory, ConfigurablePrinter configuration) {
+        this.configuration = configuration;
+        this.visitorFactory = visitorFactory;
+    }
+    
+    // Methods
+    
+    /*
+     * Returns the Printer configuration
+     */
+    public ConfigurablePrinter getConfiguration() {
+        return configuration;
+    }
+
+    /*
+     * set or update the PrettyPrinter configuration
+     */
+    public Printable setConfiguration(ConfigurablePrinter configuration) {
+        this.configuration = configuration;
+        return this;
+    }
+
+    @Override
+    public String print(Node node) {
+        // lazy initialization of visitor which can have a state (like a buffer)
+        VoidVisitor<Void> visitor = visitorFactory.apply(configuration);
+        node.accept(visitor, null);
+        return visitor.toString();
+    }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinter.java b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinter.java
index f79217f..28300a1 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinter.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinter.java
@@ -21,41 +21,60 @@
 
 package com.github.javaparser.printer;
 
+import java.util.function.Function;
+
 import com.github.javaparser.ast.Node;
 import com.github.javaparser.ast.visitor.VoidVisitor;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;
 
 /**
  * Pretty printer for AST nodes.
+ * This class is no longer acceptable to use because it is not sufficiently configurable and it is too tied to a specific implementation
+ * <p> Use {@link Printable interface or PrettyPrintable default implementation } instead.
  */
+@Deprecated
 public class PrettyPrinter implements Printable {
-    private PrettyPrinterConfiguration configuration;
+    
+    private ConfigurablePrinter configuration;
+    
+    private Function<PrettyPrinterConfiguration, VoidVisitor<Void>> visitorFactory;
 
     public PrettyPrinter() {
         this(new PrettyPrinterConfiguration());
     }
-
+    
     public PrettyPrinter(PrettyPrinterConfiguration configuration) {
-        this.configuration = configuration;
+        this(configuration, PrettyPrintVisitor::new);
     }
-
+    
+    public PrettyPrinter(PrettyPrinterConfiguration configuration, Function<PrettyPrinterConfiguration, VoidVisitor<Void>> visitorFactory) {
+        this.configuration = configuration;
+        this.visitorFactory = visitorFactory;
+    }
+    
     /*
      * Returns the PrettyPrinter configuration
      */
-    public PrettyPrinterConfiguration getConfiguration() {
+    public ConfigurablePrinter getConfiguration() {
         return configuration;
     }
 
     /*
      * set or update the PrettyPrinter configuration
      */
-    public void setConfiguration(PrettyPrinterConfiguration configuration) {
+    public Printable setConfiguration(ConfigurablePrinter configuration) {
+        if (!(configuration instanceof PrettyPrinterConfiguration))
+            throw new IllegalArgumentException("PrettyPrinter must be configured with a PrettyPrinterConfiguration class");
         this.configuration = configuration;
+        return this;
     }
 
     @Override
     public String print(Node node) {
-        final VoidVisitor<Void> visitor = configuration.getVisitorFactory().apply(configuration);
+        final VoidVisitor<Void> visitor = visitorFactory.apply((PrettyPrinterConfiguration)configuration);
         node.accept(visitor, null);
         return visitor.toString();
     }
+
 }
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/Printable.java b/javaparser-core/src/main/java/com/github/javaparser/printer/Printable.java
index 660a178..c8a9fab 100755
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/Printable.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/Printable.java
@@ -1,9 +1,14 @@
 package com.github.javaparser.printer;
 
 import com.github.javaparser.ast.Node;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
 
 public interface Printable {
 
     String print(Node node);
 
+    Printable setConfiguration(ConfigurablePrinter configuration);
+    
+    ConfigurablePrinter getConfiguration();
+
 }
\ No newline at end of file
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableSource.java b/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableSource.java
new file mode 100755
index 0000000..547ee27
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableSource.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2011, 2013-2020 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer;
+
+import com.github.javaparser.Position;
+
+public interface PrintableSource {
+
+    /**
+     * Add the default indentation to the current indentation and push it on the indentation stack.
+     * Does not actually output anything.
+     */
+    PrintableSource indent();
+
+    /**
+     * Add to the current indentation until it is reaches "column" and push it on the indentation stack.
+     * Does not actually output anything.
+     */
+    PrintableSource indentWithAlignTo(int column);
+
+    /**
+     * Pop the last indentation of the indentation stack.
+     * Does not actually output anything.
+     */
+    PrintableSource unindent();
+
+    /**
+     * Append the source string passed as argument to the buffer.
+     * If this is being appended at the beginning of a line, performs indentation first.
+     * <p>
+     * The source line to be printed should not contain newline/carriage-return characters;
+     * use {@link #println(String)} to automatically append a newline at the end of the source string.
+     * If the source line passed as argument contains newline/carriage-return characters would
+     * impredictably affect a correct computation of the current {@link #getCursor()} position.
+     *
+     * @param arg source line to be printed (should not contain newline/carriage-return characters)
+     * @return this instance, for nesting calls to method as fluent interface
+     * @see PrintableSource#println(String)
+     */
+    PrintableSource print(String arg);
+
+    /**
+     * Append the source string passed as argument to the buffer, then append a newline.
+     * If this is being appended at the beginning of a line, performs indentation first.
+     * <p>
+     * The source line to be printed should not contain newline/carriage-return characters.
+     * If the source line passed as argument contains newline/carriage-return characters would
+     * impredictably affect a correct computation of the current {@link #getCursor()} position.
+     *
+     * @param arg source line to be printed (should not contain newline/carriage-return characters)
+     * @return this instance, for nesting calls to method as fluent interface
+     */
+    PrintableSource println(String arg);
+
+    /**
+     * Append a newline to the buffer.
+     *
+     * @return this instance, for nesting calls to method as fluent interface
+     */
+    PrintableSource println();
+
+    /**
+     * Return the current cursor position (line, column) in the source printer buffer.
+     * <p>
+     * Please notice in order to guarantee a correct computation of the cursor position,
+     * this printer expect the contracts of the methods {@link #print(String)} and {@link #println(String)}
+     * has been respected through all method calls, meaning the source string passed as argument to those method
+     * calls did not contain newline/carriage-return characters.
+     *
+     * @return the current cursor position (line, column).
+     */
+    Position getCursor();
+
+    /**
+     * @return the currently printed source code.
+     */
+    String toString();
+
+    /**
+     * Changes all EOL characters in "content" to the EOL character this SourcePrinter is using.
+     */
+    String normalizeEolInTextBlock(String content);
+
+    /**
+     * Set the top-most indent to the column the cursor is currently in, can be undone with
+     * {@link #reindentToPreviousLevel()}. Does not actually output anything.
+     */
+    void reindentWithAlignToCursor();
+
+    /**
+     * Set the top-most indent to the column the cursor was before the last {@link #reindentWithAlignToCursor()} call.
+     * Does not actually output anything.
+     */
+    void reindentToPreviousLevel();
+
+    /**
+     * Adds an indent to the top of the stack that is a copy of the current top indent.
+     * With this you announce "I'm going to indent the next line(s)" but not how far yet.
+     * Once you do know, you can pop this indent ("unindent") and indent to the right column.
+     * (Does not actually output anything.)
+     */
+    void duplicateIndent();
+
+}
\ No newline at end of file
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableVisitor.java b/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableVisitor.java
new file mode 100755
index 0000000..8ce3869
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/PrintableVisitor.java
@@ -0,0 +1,1944 @@
+/*
+ * Copyright (C) 2011, 2013-2020 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer;
+
+import static com.github.javaparser.ast.Node.Parsedness.UNPARSABLE;
+import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition;
+import static com.github.javaparser.utils.Utils.isNullOrEmpty;
+import static com.github.javaparser.utils.Utils.normalizeEolInTextBlock;
+import static com.github.javaparser.utils.Utils.trimTrailingSpaces;
+import static java.util.Comparator.comparingInt;
+import static java.util.stream.Collectors.joining;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import com.github.javaparser.ast.ArrayCreationLevel;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.ImportDeclaration;
+import com.github.javaparser.ast.Modifier;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.PackageDeclaration;
+import com.github.javaparser.ast.body.AnnotationDeclaration;
+import com.github.javaparser.ast.body.AnnotationMemberDeclaration;
+import com.github.javaparser.ast.body.BodyDeclaration;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.body.ConstructorDeclaration;
+import com.github.javaparser.ast.body.EnumConstantDeclaration;
+import com.github.javaparser.ast.body.EnumDeclaration;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.InitializerDeclaration;
+import com.github.javaparser.ast.body.MethodDeclaration;
+import com.github.javaparser.ast.body.Parameter;
+import com.github.javaparser.ast.body.ReceiverParameter;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.body.VariableDeclarator;
+import com.github.javaparser.ast.comments.BlockComment;
+import com.github.javaparser.ast.comments.Comment;
+import com.github.javaparser.ast.comments.JavadocComment;
+import com.github.javaparser.ast.comments.LineComment;
+import com.github.javaparser.ast.expr.AnnotationExpr;
+import com.github.javaparser.ast.expr.ArrayAccessExpr;
+import com.github.javaparser.ast.expr.ArrayCreationExpr;
+import com.github.javaparser.ast.expr.ArrayInitializerExpr;
+import com.github.javaparser.ast.expr.AssignExpr;
+import com.github.javaparser.ast.expr.BinaryExpr;
+import com.github.javaparser.ast.expr.BooleanLiteralExpr;
+import com.github.javaparser.ast.expr.CastExpr;
+import com.github.javaparser.ast.expr.CharLiteralExpr;
+import com.github.javaparser.ast.expr.ClassExpr;
+import com.github.javaparser.ast.expr.ConditionalExpr;
+import com.github.javaparser.ast.expr.DoubleLiteralExpr;
+import com.github.javaparser.ast.expr.EnclosedExpr;
+import com.github.javaparser.ast.expr.Expression;
+import com.github.javaparser.ast.expr.FieldAccessExpr;
+import com.github.javaparser.ast.expr.InstanceOfExpr;
+import com.github.javaparser.ast.expr.IntegerLiteralExpr;
+import com.github.javaparser.ast.expr.LambdaExpr;
+import com.github.javaparser.ast.expr.LongLiteralExpr;
+import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
+import com.github.javaparser.ast.expr.MemberValuePair;
+import com.github.javaparser.ast.expr.MethodCallExpr;
+import com.github.javaparser.ast.expr.MethodReferenceExpr;
+import com.github.javaparser.ast.expr.Name;
+import com.github.javaparser.ast.expr.NameExpr;
+import com.github.javaparser.ast.expr.NormalAnnotationExpr;
+import com.github.javaparser.ast.expr.NullLiteralExpr;
+import com.github.javaparser.ast.expr.ObjectCreationExpr;
+import com.github.javaparser.ast.expr.PatternExpr;
+import com.github.javaparser.ast.expr.SimpleName;
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
+import com.github.javaparser.ast.expr.StringLiteralExpr;
+import com.github.javaparser.ast.expr.SuperExpr;
+import com.github.javaparser.ast.expr.SwitchExpr;
+import com.github.javaparser.ast.expr.TextBlockLiteralExpr;
+import com.github.javaparser.ast.expr.ThisExpr;
+import com.github.javaparser.ast.expr.TypeExpr;
+import com.github.javaparser.ast.expr.UnaryExpr;
+import com.github.javaparser.ast.expr.VariableDeclarationExpr;
+import com.github.javaparser.ast.modules.ModuleDeclaration;
+import com.github.javaparser.ast.modules.ModuleExportsDirective;
+import com.github.javaparser.ast.modules.ModuleOpensDirective;
+import com.github.javaparser.ast.modules.ModuleProvidesDirective;
+import com.github.javaparser.ast.modules.ModuleRequiresDirective;
+import com.github.javaparser.ast.modules.ModuleUsesDirective;
+import com.github.javaparser.ast.nodeTypes.NodeWithName;
+import com.github.javaparser.ast.nodeTypes.NodeWithTraversableScope;
+import com.github.javaparser.ast.nodeTypes.NodeWithTypeArguments;
+import com.github.javaparser.ast.nodeTypes.NodeWithVariables;
+import com.github.javaparser.ast.nodeTypes.SwitchNode;
+import com.github.javaparser.ast.stmt.AssertStmt;
+import com.github.javaparser.ast.stmt.BlockStmt;
+import com.github.javaparser.ast.stmt.BreakStmt;
+import com.github.javaparser.ast.stmt.CatchClause;
+import com.github.javaparser.ast.stmt.ContinueStmt;
+import com.github.javaparser.ast.stmt.DoStmt;
+import com.github.javaparser.ast.stmt.EmptyStmt;
+import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt;
+import com.github.javaparser.ast.stmt.ExpressionStmt;
+import com.github.javaparser.ast.stmt.ForEachStmt;
+import com.github.javaparser.ast.stmt.ForStmt;
+import com.github.javaparser.ast.stmt.IfStmt;
+import com.github.javaparser.ast.stmt.LabeledStmt;
+import com.github.javaparser.ast.stmt.LocalClassDeclarationStmt;
+import com.github.javaparser.ast.stmt.ReturnStmt;
+import com.github.javaparser.ast.stmt.Statement;
+import com.github.javaparser.ast.stmt.SwitchEntry;
+import com.github.javaparser.ast.stmt.SwitchStmt;
+import com.github.javaparser.ast.stmt.SynchronizedStmt;
+import com.github.javaparser.ast.stmt.ThrowStmt;
+import com.github.javaparser.ast.stmt.TryStmt;
+import com.github.javaparser.ast.stmt.UnparsableStmt;
+import com.github.javaparser.ast.stmt.WhileStmt;
+import com.github.javaparser.ast.stmt.YieldStmt;
+import com.github.javaparser.ast.type.ArrayType;
+import com.github.javaparser.ast.type.ClassOrInterfaceType;
+import com.github.javaparser.ast.type.IntersectionType;
+import com.github.javaparser.ast.type.PrimitiveType;
+import com.github.javaparser.ast.type.ReferenceType;
+import com.github.javaparser.ast.type.Type;
+import com.github.javaparser.ast.type.TypeParameter;
+import com.github.javaparser.ast.type.UnionType;
+import com.github.javaparser.ast.type.UnknownType;
+import com.github.javaparser.ast.type.VarType;
+import com.github.javaparser.ast.type.VoidType;
+import com.github.javaparser.ast.type.WildcardType;
+import com.github.javaparser.ast.visitor.Visitable;
+import com.github.javaparser.ast.visitor.VoidVisitor;
+import com.github.javaparser.printer.configuration.ConfigurablePrinter;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
+
+/**
+ * Outputs the AST as formatted Java source code.
+ *
+ */
+public class PrintableVisitor implements VoidVisitor<Void> {
+    protected final ConfigurablePrinter configuration;
+    protected final PrintableSource printer;
+
+    public PrintableVisitor(ConfigurablePrinter configuration) {
+        this(configuration, new DefaultPrintableSource(configuration));
+    }
+    
+    public PrintableVisitor(ConfigurablePrinter configuration, PrintableSource printer) {
+        this.configuration = configuration;
+        this.printer = printer;
+    }
+
+    @Override
+    public String toString() {
+        return printer.toString();
+    }
+
+    protected void printModifiers(final NodeList<Modifier> modifiers) {
+        if (modifiers.size() > 0) {
+            printer.print(modifiers.stream().map(Modifier::getKeyword).map(Modifier.Keyword::asString).collect(joining(" ")) + " ");
+        }
+    }
+
+    protected void printMembers(final NodeList<BodyDeclaration<?>> members, final Void arg) {
+        for (final BodyDeclaration<?> member : members) {
+            printer.println();
+            member.accept(this, arg);
+            printer.println();
+        }
+    }
+
+    protected void printMemberAnnotations(final NodeList<AnnotationExpr> annotations, final Void arg) {
+        if (annotations.isEmpty()) {
+            return;
+        }
+        for (final AnnotationExpr a : annotations) {
+            a.accept(this, arg);
+            printer.println();
+        }
+    }
+
+    protected void printAnnotations(final NodeList<AnnotationExpr> annotations, boolean prefixWithASpace,
+                                  final Void arg) {
+        if (annotations.isEmpty()) {
+            return;
+        }
+        if (prefixWithASpace) {
+            printer.print(" ");
+        }
+        for (AnnotationExpr annotation : annotations) {
+            annotation.accept(this, arg);
+            printer.print(" ");
+        }
+    }
+
+    protected void printTypeArgs(final NodeWithTypeArguments<?> nodeWithTypeArguments, final Void arg) {
+        NodeList<Type> typeArguments = nodeWithTypeArguments.getTypeArguments().orElse(null);
+        if (!isNullOrEmpty(typeArguments)) {
+            printer.print("<");
+            for (final Iterator<Type> i = typeArguments.iterator(); i.hasNext(); ) {
+                final Type t = i.next();
+                t.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+            printer.print(">");
+        }
+    }
+
+    protected void printTypeParameters(final NodeList<TypeParameter> args, final Void arg) {
+        if (!isNullOrEmpty(args)) {
+            printer.print("<");
+            for (final Iterator<TypeParameter> i = args.iterator(); i.hasNext(); ) {
+                final TypeParameter t = i.next();
+                t.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+            printer.print(">");
+        }
+    }
+
+    protected void printArguments(final NodeList<Expression> args, final Void arg) {
+        printer.print("(");
+        if (!isNullOrEmpty(args)) {
+            boolean columnAlignParameters = (args.size() > 1) && configuration.get(ConfigOption.COLUMN_ALIGN_PARAMETERS).isPresent();
+            if (columnAlignParameters) {
+                printer.indentWithAlignTo(printer.getCursor().column);
+            }
+            for (final Iterator<Expression> i = args.iterator(); i.hasNext(); ) {
+                final Expression e = i.next();
+                e.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(",");
+                    if (columnAlignParameters) {
+                        printer.println();
+                    } else {
+                        printer.print(" ");
+                    }
+                }
+            }
+            if (columnAlignParameters) {
+                printer.unindent();
+            }
+        }
+        printer.print(")");
+    }
+
+    protected void printPrePostFixOptionalList(final NodeList<? extends Visitable> args, final Void arg, String prefix, String separator, String postfix) {
+        if (!args.isEmpty()) {
+            printer.print(prefix);
+            for (final Iterator<? extends Visitable> i = args.iterator(); i.hasNext(); ) {
+                final Visitable v = i.next();
+                v.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(separator);
+                }
+            }
+            printer.print(postfix);
+        }
+    }
+
+    protected void printPrePostFixRequiredList(final NodeList<? extends Visitable> args, final Void arg, String prefix, String separator, String postfix) {
+        printer.print(prefix);
+        if (!args.isEmpty()) {
+            for (final Iterator<? extends Visitable> i = args.iterator(); i.hasNext(); ) {
+                final Visitable v = i.next();
+                v.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(separator);
+                }
+            }
+        }
+        printer.print(postfix);
+    }
+
+    protected void printComment(final Optional<Comment> comment, final Void arg) {
+        comment.ifPresent(c -> c.accept(this, arg));
+    }
+
+    @Override
+    public void visit(final CompilationUnit n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getParsed() == UNPARSABLE) {
+            printer.println("???");
+            return;
+        }
+
+        if (n.getPackageDeclaration().isPresent()) {
+            n.getPackageDeclaration().get().accept(this, arg);
+        }
+
+        n.getImports().accept(this, arg);
+        if (!n.getImports().isEmpty()) {
+            printer.println();
+        }
+
+        for (final Iterator<TypeDeclaration<?>> i = n.getTypes().iterator(); i.hasNext(); ) {
+            i.next().accept(this, arg);
+            printer.println();
+            if (i.hasNext()) {
+                printer.println();
+            }
+        }
+
+        n.getModule().ifPresent(m -> m.accept(this, arg));
+
+        printOrphanCommentsEnding(n);
+    }
+
+    @Override
+    public void visit(final PackageDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printer.print("package ");
+        n.getName().accept(this, arg);
+        printer.println(";");
+        printer.println();
+
+        printOrphanCommentsEnding(n);
+    }
+
+    @Override
+    public void visit(final NameExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getName().accept(this, arg);
+
+        printOrphanCommentsEnding(n);
+    }
+
+    @Override
+    public void visit(final Name n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getQualifier().isPresent()) {
+            n.getQualifier().get().accept(this, arg);
+            printer.print(".");
+        }
+        printer.print(n.getIdentifier());
+
+        printOrphanCommentsEnding(n);
+    }
+
+    @Override
+    public void visit(SimpleName n, Void arg) {
+        printer.print(n.getIdentifier());
+    }
+
+    @Override
+    public void visit(final ClassOrInterfaceDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+
+        if (n.isInterface()) {
+            printer.print("interface ");
+        } else {
+            printer.print("class ");
+        }
+
+        n.getName().accept(this, arg);
+
+        printTypeParameters(n.getTypeParameters(), arg);
+
+        if (!n.getExtendedTypes().isEmpty()) {
+            printer.print(" extends ");
+            for (final Iterator<ClassOrInterfaceType> i = n.getExtendedTypes().iterator(); i.hasNext(); ) {
+                final ClassOrInterfaceType c = i.next();
+                c.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+
+        if (!n.getImplementedTypes().isEmpty()) {
+            printer.print(" implements ");
+            for (final Iterator<ClassOrInterfaceType> i = n.getImplementedTypes().iterator(); i.hasNext(); ) {
+                final ClassOrInterfaceType c = i.next();
+                c.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+
+        printer.println(" {");
+        printer.indent();
+        if (!isNullOrEmpty(n.getMembers())) {
+            printMembers(n.getMembers(), arg);
+        }
+
+        printOrphanCommentsEnding(n);
+
+        printer.unindent();
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final JavadocComment n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        if (configuration.get(ConfigOption.PRINT_COMMENTS).isPresent() && configuration.get(ConfigOption.PRINT_JAVADOC).isPresent()) {
+            printer.println("/**");
+            final String commentContent = normalizeEolInTextBlock(n.getContent(), configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString());
+            String[] lines = commentContent.split("\\R");
+            List<String> strippedLines = new ArrayList<>();
+            for (String line : lines) {
+                final String trimmedLine = line.trim();
+                if (trimmedLine.startsWith("*")) {
+                    line = trimmedLine.substring(1);
+                }
+                line = trimTrailingSpaces(line);
+                strippedLines.add(line);
+            }
+
+            boolean skippingLeadingEmptyLines = true;
+            boolean prependEmptyLine = false;
+            boolean prependSpace = strippedLines.stream().anyMatch(line -> !line.isEmpty() && !line.startsWith(" "));
+            for (String line : strippedLines) {
+                if (line.isEmpty()) {
+                    if (!skippingLeadingEmptyLines) {
+                        prependEmptyLine = true;
+                    }
+                } else {
+                    skippingLeadingEmptyLines = false;
+                    if (prependEmptyLine) {
+                        printer.println(" *");
+                        prependEmptyLine = false;
+                    }
+                    printer.print(" *");
+                    if (prependSpace) {
+                        printer.print(" ");
+                    }
+                    printer.println(line);
+                }
+            }
+            printer.println(" */");
+        }
+    }
+
+    @Override
+    public void visit(final ClassOrInterfaceType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getScope().isPresent()) {
+            n.getScope().get().accept(this, arg);
+            printer.print(".");
+        }
+        printAnnotations(n.getAnnotations(), false, arg);
+
+        n.getName().accept(this, arg);
+
+        if (n.isUsingDiamondOperator()) {
+            printer.print("<>");
+        } else {
+            printTypeArgs(n, arg);
+        }
+    }
+
+    @Override
+    public void visit(final TypeParameter n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        n.getName().accept(this, arg);
+        if (!isNullOrEmpty(n.getTypeBound())) {
+            printer.print(" extends ");
+            for (final Iterator<ClassOrInterfaceType> i = n.getTypeBound().iterator(); i.hasNext(); ) {
+                final ClassOrInterfaceType c = i.next();
+                c.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(" & ");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void visit(final PrimitiveType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), true, arg);
+        printer.print(n.getType().asString());
+    }
+
+    @Override
+    public void visit(final ArrayType n, final Void arg) {
+        final List<ArrayType> arrayTypeBuffer = new LinkedList<>();
+        Type type = n;
+        while (type instanceof ArrayType) {
+            final ArrayType arrayType = (ArrayType) type;
+            arrayTypeBuffer.add(arrayType);
+            type = arrayType.getComponentType();
+        }
+
+        type.accept(this, arg);
+        for (ArrayType arrayType : arrayTypeBuffer) {
+            printAnnotations(arrayType.getAnnotations(), true, arg);
+            printer.print("[]");
+        }
+    }
+
+    @Override
+    public void visit(final ArrayCreationLevel n, final Void arg) {
+        printAnnotations(n.getAnnotations(), true, arg);
+        printer.print("[");
+        if (n.getDimension().isPresent()) {
+            n.getDimension().get().accept(this, arg);
+        }
+        printer.print("]");
+    }
+
+    @Override
+    public void visit(final IntersectionType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        boolean isFirst = true;
+        for (ReferenceType element : n.getElements()) {
+            if (isFirst) {
+                isFirst = false;
+            } else {
+                printer.print(" & ");
+            }
+            element.accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final UnionType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), true, arg);
+        boolean isFirst = true;
+        for (ReferenceType element : n.getElements()) {
+            if (isFirst) {
+                isFirst = false;
+            } else {
+                printer.print(" | ");
+            }
+            element.accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final WildcardType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        printer.print("?");
+        if (n.getExtendedType().isPresent()) {
+            printer.print(" extends ");
+            n.getExtendedType().get().accept(this, arg);
+        }
+        if (n.getSuperType().isPresent()) {
+            printer.print(" super ");
+            n.getSuperType().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final UnknownType n, final Void arg) {
+        // Nothing to print
+    }
+
+    @Override
+    public void visit(final FieldDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+        if (!n.getVariables().isEmpty()) {
+            Optional<Type> maximumCommonType = n.getMaximumCommonType();
+            maximumCommonType.ifPresent(t -> t.accept(this, arg));
+            if (!maximumCommonType.isPresent()) {
+                printer.print("???");
+            }
+        }
+
+        printer.print(" ");
+        for (final Iterator<VariableDeclarator> i = n.getVariables().iterator(); i.hasNext(); ) {
+            final VariableDeclarator var = i.next();
+            var.accept(this, arg);
+            if (i.hasNext()) {
+                printer.print(", ");
+            }
+        }
+
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final VariableDeclarator n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getName().accept(this, arg);
+
+        n.findAncestor(NodeWithVariables.class).ifPresent(ancestor -> ((NodeWithVariables<?>) ancestor).getMaximumCommonType().ifPresent(commonType -> {
+
+            final Type type = n.getType();
+
+            ArrayType arrayType = null;
+
+            for (int i = commonType.getArrayLevel(); i < type.getArrayLevel(); i++) {
+                if (arrayType == null) {
+                    arrayType = (ArrayType) type;
+                } else {
+                    arrayType = (ArrayType) arrayType.getComponentType();
+                }
+                printAnnotations(arrayType.getAnnotations(), true, arg);
+                printer.print("[]");
+            }
+        }));
+
+        if (n.getInitializer().isPresent()) {
+            printer.print(" = ");
+            n.getInitializer().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final ArrayInitializerExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("{");
+        if (!isNullOrEmpty(n.getValues())) {
+            printer.print(" ");
+            for (final Iterator<Expression> i = n.getValues().iterator(); i.hasNext(); ) {
+                final Expression expr = i.next();
+                expr.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+            printer.print(" ");
+        }
+        printOrphanCommentsEnding(n);
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final VoidType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        printer.print("void");
+    }
+
+    @Override
+    public void visit(final VarType n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        printer.print("var");
+    }
+
+    @Override
+    public void visit(Modifier n, Void arg) {
+        printer.print(n.getKeyword().asString());
+        printer.print(" ");
+    }
+
+    @Override
+    public void visit(final ArrayAccessExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getName().accept(this, arg);
+        printer.print("[");
+        n.getIndex().accept(this, arg);
+        printer.print("]");
+    }
+
+    @Override
+    public void visit(final ArrayCreationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("new ");
+        n.getElementType().accept(this, arg);
+        for (ArrayCreationLevel level : n.getLevels()) {
+            level.accept(this, arg);
+        }
+        if (n.getInitializer().isPresent()) {
+            printer.print(" ");
+            n.getInitializer().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final AssignExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getTarget().accept(this, arg);
+        if (configuration.get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent()) {
+            printer.print(" ");
+        }
+        printer.print(n.getOperator().asString());
+        if (configuration.get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent()) {
+            printer.print(" ");
+        }
+        n.getValue().accept(this, arg);
+    }
+
+
+
+    /**
+     * work in progress for issue-545
+     */
+
+    @Override
+    public void visit(final BinaryExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getLeft().accept(this, arg);
+        if (configuration.get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent()) {
+            printer.print(" ");
+        }
+        printer.print(n.getOperator().asString());
+        if (configuration.get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent()) {
+            printer.print(" ");
+        }
+        n.getRight().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final CastExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("(");
+        n.getType().accept(this, arg);
+        printer.print(") ");
+        n.getExpression().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ClassExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getType().accept(this, arg);
+        printer.print(".class");
+    }
+
+    @Override
+    public void visit(final ConditionalExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getCondition().accept(this, arg);
+        printer.print(" ? ");
+        n.getThenExpr().accept(this, arg);
+        printer.print(" : ");
+        n.getElseExpr().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final EnclosedExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("(");
+        n.getInner().accept(this, arg);
+        printer.print(")");
+    }
+
+    @Override
+    public void visit(final FieldAccessExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getScope().accept(this, arg);
+        printer.print(".");
+        n.getName().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final InstanceOfExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getExpression().accept(this, arg);
+        printer.print(" instanceof ");
+        n.getType().accept(this, arg);
+        if(n.getName().isPresent()) {
+            printer.print(" ");
+            n.getName().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final PatternExpr n, final Void arg) {
+        n.getType().accept(this, arg);
+        printer.print(" ");
+        n.getName().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final CharLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("'");
+        printer.print(n.getValue());
+        printer.print("'");
+    }
+
+    @Override
+    public void visit(final DoubleLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(n.getValue());
+    }
+
+    @Override
+    public void visit(final IntegerLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(n.getValue());
+    }
+
+    @Override
+    public void visit(final LongLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(n.getValue());
+    }
+
+    @Override
+    public void visit(final StringLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("\"");
+        printer.print(n.getValue());
+        printer.print("\"");
+    }
+
+    @Override
+    public void visit(final TextBlockLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("\"\"\"");
+        printer.indent();
+        n.stripIndentOfLines().forEach(line -> {
+            printer.println();
+            printer.print(line);
+        });
+        printer.print("\"\"\"");
+        printer.unindent();
+    }
+
+    @Override
+    public void visit(final BooleanLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(String.valueOf(n.getValue()));
+    }
+
+    @Override
+    public void visit(final NullLiteralExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("null");
+    }
+
+    @Override
+    public void visit(final ThisExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getTypeName().isPresent()) {
+            n.getTypeName().get().accept(this, arg);
+            printer.print(".");
+        }
+        printer.print("this");
+    }
+
+    @Override
+    public void visit(final SuperExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getTypeName().isPresent()) {
+            n.getTypeName().get().accept(this, arg);
+            printer.print(".");
+        }
+        printer.print("super");
+    }
+
+    @Override
+    public void visit(final MethodCallExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+
+        // determine whether we do reindenting for aligmnent at all
+        // - is it enabled?
+        // - are we in a statement where we want the alignment?
+        // - are we not directly in the argument list of a method call expression?
+        AtomicBoolean columnAlignFirstMethodChain = new AtomicBoolean();
+        if (configuration.get(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN).isPresent()) {
+            // pick the kind of expressions where vertically aligning method calls is okay.
+            if (n.findAncestor(Statement.class).map(p -> p.isReturnStmt()
+                    || p.isThrowStmt()
+                    || p.isAssertStmt()
+                    || p.isExpressionStmt()).orElse(false)) {
+                // search for first parent that does not have its child as scope
+                Node c = n;
+                Optional<Node> p = c.getParentNode();
+                while (p.isPresent() && p.filter(NodeWithTraversableScope.class::isInstance)
+                        .map(NodeWithTraversableScope.class::cast)
+                        .flatMap(NodeWithTraversableScope::traverseScope)
+                        .map(c::equals)
+                        .orElse(false)) {
+                    c = p.get();
+                    p = c.getParentNode();
+                }
+
+                // check if the parent is a method call and thus we are in an argument list
+                columnAlignFirstMethodChain.set(!p.filter(MethodCallExpr.class::isInstance).isPresent());
+            }
+        }
+
+        // we are at the last method call of a call chain
+        // this means we do not start reindenting for alignment or we undo it
+        AtomicBoolean lastMethodInCallChain = new AtomicBoolean(true);
+        if (columnAlignFirstMethodChain.get()) {
+            Node node = n;
+            while (node.getParentNode()
+                    .filter(NodeWithTraversableScope.class::isInstance)
+                    .map(NodeWithTraversableScope.class::cast)
+                    .flatMap(NodeWithTraversableScope::traverseScope)
+                    .map(node::equals)
+                    .orElse(false)) {
+                node = node.getParentNode().orElseThrow(AssertionError::new);
+                if (node instanceof MethodCallExpr) {
+                    lastMethodInCallChain.set(false);
+                    break;
+                }
+            }
+        }
+
+        // search whether there is a method call with scope in the scope already
+        // this means that we probably started reindenting for alignment there
+        AtomicBoolean methodCallWithScopeInScope = new AtomicBoolean();
+        if (columnAlignFirstMethodChain.get()) {
+            Optional<Expression> s = n.getScope();
+            while (s.filter(NodeWithTraversableScope.class::isInstance).isPresent()) {
+                Optional<Expression> parentScope = s.map(NodeWithTraversableScope.class::cast)
+                        .flatMap(NodeWithTraversableScope::traverseScope);
+                if (s.filter(MethodCallExpr.class::isInstance).isPresent() && parentScope.isPresent()) {
+                    methodCallWithScopeInScope.set(true);
+                    break;
+                }
+                s = parentScope;
+            }
+        }
+
+        // we have a scope
+        // this means we are not the first method in the chain
+        n.getScope().ifPresent(scope -> {
+            scope.accept(this, arg);
+            if (columnAlignFirstMethodChain.get()) {
+                if (methodCallWithScopeInScope.get()) {
+                    /* We're a method call on the result of something (method call, property access, ...) that is not stand alone,
+                       and not the first one with scope, like:
+                       we're x() in a.b().x(), or in a=b().c[15].d.e().x().
+                       That means that the "else" has been executed by one of the methods in the scope chain, so that the alignment
+                       is set to the "." of that method.
+                       That means we will align to that "." when we start a new line: */
+                    printer.println();
+                } else if (!lastMethodInCallChain.get()) {
+                    /* We're the first method call on the result of something in the chain (method call, property access, ...),
+                       but we are not at the same time the last method call in that chain, like:
+                       we're x() in a().x().y(), or in Long.x().y.z(). That means we get to dictate the indent of following method
+                       calls in this chain by setting the cursor to where we are now: just before the "."
+                       that start this method call. */
+                    printer.reindentWithAlignToCursor();
+                }
+            }
+            printer.print(".");
+        });
+
+        printTypeArgs(n, arg);
+        n.getName().accept(this, arg);
+        printer.duplicateIndent();
+        printArguments(n.getArguments(), arg);
+        printer.unindent();
+        if (columnAlignFirstMethodChain.get() && methodCallWithScopeInScope.get() && lastMethodInCallChain.get()) {
+            // undo the aligning after the arguments of the last method call are printed
+            printer.reindentToPreviousLevel();
+        }
+    }
+
+    @Override
+    public void visit(final ObjectCreationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getScope().isPresent()) {
+            n.getScope().get().accept(this, arg);
+            printer.print(".");
+        }
+
+        printer.print("new ");
+
+        printTypeArgs(n, arg);
+        if (!isNullOrEmpty(n.getTypeArguments().orElse(null))) {
+            printer.print(" ");
+        }
+
+        n.getType().accept(this, arg);
+
+        printArguments(n.getArguments(), arg);
+
+        if (n.getAnonymousClassBody().isPresent()) {
+            printer.println(" {");
+            printer.indent();
+            printMembers(n.getAnonymousClassBody().get(), arg);
+            printer.unindent();
+            printer.print("}");
+        }
+    }
+
+    @Override
+    public void visit(final UnaryExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getOperator().isPrefix()) {
+            printer.print(n.getOperator().asString());
+        }
+
+        n.getExpression().accept(this, arg);
+
+        if (n.getOperator().isPostfix()) {
+            printer.print(n.getOperator().asString());
+        }
+    }
+
+    @Override
+    public void visit(final ConstructorDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+
+        printTypeParameters(n.getTypeParameters(), arg);
+        if (n.isGeneric()) {
+            printer.print(" ");
+        }
+        n.getName().accept(this, arg);
+
+        printer.print("(");
+        if (!n.getParameters().isEmpty()) {
+            for (final Iterator<Parameter> i = n.getParameters().iterator(); i.hasNext(); ) {
+                final Parameter p = i.next();
+                p.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print(")");
+
+        if (!isNullOrEmpty(n.getThrownExceptions())) {
+            printer.print(" throws ");
+            for (final Iterator<ReferenceType> i = n.getThrownExceptions().iterator(); i.hasNext(); ) {
+                final ReferenceType name = i.next();
+                name.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print(" ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final MethodDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+        printTypeParameters(n.getTypeParameters(), arg);
+        if (!isNullOrEmpty(n.getTypeParameters())) {
+            printer.print(" ");
+        }
+
+        n.getType().accept(this, arg);
+        printer.print(" ");
+        n.getName().accept(this, arg);
+
+        printer.print("(");
+        n.getReceiverParameter().ifPresent(rp -> {
+            rp.accept(this, arg);
+            if (!isNullOrEmpty(n.getParameters())) {
+                printer.print(", ");
+            }
+        });
+        if (!isNullOrEmpty(n.getParameters())) {
+            for (final Iterator<Parameter> i = n.getParameters().iterator(); i.hasNext(); ) {
+                final Parameter p = i.next();
+                p.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print(")");
+
+        if (!isNullOrEmpty(n.getThrownExceptions())) {
+            printer.print(" throws ");
+            for (final Iterator<ReferenceType> i = n.getThrownExceptions().iterator(); i.hasNext(); ) {
+                final ReferenceType name = i.next();
+                name.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        if (!n.getBody().isPresent()) {
+            printer.print(";");
+        } else {
+            printer.print(" ");
+            n.getBody().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final Parameter n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        printModifiers(n.getModifiers());
+        n.getType().accept(this, arg);
+        if (n.isVarArgs()) {
+            printAnnotations(n.getVarArgsAnnotations(), false, arg);
+            printer.print("...");
+        }
+        if (!(n.getType() instanceof UnknownType)) {
+            printer.print(" ");
+        }
+        n.getName().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ReceiverParameter n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printAnnotations(n.getAnnotations(), false, arg);
+        n.getType().accept(this, arg);
+        printer.print(" ");
+        n.getName().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ExplicitConstructorInvocationStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.isThis()) {
+            printTypeArgs(n, arg);
+            printer.print("this");
+        } else {
+            if (n.getExpression().isPresent()) {
+                n.getExpression().get().accept(this, arg);
+                printer.print(".");
+            }
+            printTypeArgs(n, arg);
+            printer.print("super");
+        }
+        printArguments(n.getArguments(), arg);
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final VariableDeclarationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getParentNode().map(ExpressionStmt.class::isInstance).orElse(false)) {
+            printMemberAnnotations(n.getAnnotations(), arg);
+        } else {
+            printAnnotations(n.getAnnotations(), false, arg);
+        }
+        printModifiers(n.getModifiers());
+
+        if (!n.getVariables().isEmpty()) {
+            n.getMaximumCommonType().ifPresent(t -> t.accept(this, arg));
+        }
+        printer.print(" ");
+
+        for (final Iterator<VariableDeclarator> i = n.getVariables().iterator(); i.hasNext(); ) {
+            final VariableDeclarator v = i.next();
+            v.accept(this, arg);
+            if (i.hasNext()) {
+                printer.print(", ");
+            }
+        }
+    }
+
+    @Override
+    public void visit(final LocalClassDeclarationStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getClassDeclaration().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final AssertStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("assert ");
+        n.getCheck().accept(this, arg);
+        if (n.getMessage().isPresent()) {
+            printer.print(" : ");
+            n.getMessage().get().accept(this, arg);
+        }
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final BlockStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.println("{");
+        if (n.getStatements() != null) {
+            printer.indent();
+            for (final Statement s : n.getStatements()) {
+                s.accept(this, arg);
+                printer.println();
+            }
+        }
+        printOrphanCommentsEnding(n);
+        printer.unindent();
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final LabeledStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getLabel().accept(this, arg);
+        printer.print(": ");
+        n.getStatement().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final EmptyStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final ExpressionStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getExpression().accept(this, arg);
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final SwitchStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printSwitchNode(n, arg);
+    }
+
+    @Override
+    public void visit(SwitchExpr n, Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printSwitchNode(n, arg);
+    }
+
+    private void printSwitchNode(SwitchNode n, Void arg) {
+        printComment(n.getComment(), arg);
+        printer.print("switch(");
+        n.getSelector().accept(this, arg);
+        printer.println(") {");
+        if (n.getEntries() != null) {
+            indentIf(configuration.get(ConfigOption.INDENT_CASE_IN_SWITCH).isPresent());
+            for (final SwitchEntry e : n.getEntries()) {
+                e.accept(this, arg);
+            }
+            unindentIf(configuration.get(ConfigOption.INDENT_CASE_IN_SWITCH).isPresent());
+        }
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final SwitchEntry n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+
+        if (isNullOrEmpty(n.getLabels())) {
+            printer.print("default:");
+        } else {
+            printer.print("case ");
+            for (final Iterator<Expression> i = n.getLabels().iterator(); i.hasNext(); ) {
+                final Expression label = i.next();
+                label.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+            printer.print(":");
+        }
+        printer.println();
+        printer.indent();
+        if (n.getStatements() != null) {
+            for (final Statement s : n.getStatements()) {
+                s.accept(this, arg);
+                printer.println();
+            }
+        }
+        printer.unindent();
+    }
+
+    @Override
+    public void visit(final BreakStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("break");
+        n.getLabel().ifPresent(l -> printer.print(" ").print(l.getIdentifier()));
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final YieldStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("yield ");
+        n.getExpression().accept(this, arg);
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final ReturnStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("return");
+        if (n.getExpression().isPresent()) {
+            printer.print(" ");
+            n.getExpression().get().accept(this, arg);
+        }
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final EnumDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+
+        printer.print("enum ");
+        n.getName().accept(this, arg);
+
+        if (!n.getImplementedTypes().isEmpty()) {
+            printer.print(" implements ");
+            for (final Iterator<ClassOrInterfaceType> i = n.getImplementedTypes().iterator(); i.hasNext(); ) {
+                final ClassOrInterfaceType c = i.next();
+                c.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+
+        printer.println(" {");
+        printer.indent();
+        if (n.getEntries().isNonEmpty()) {
+            final boolean alignVertically =
+                    // Either we hit the constant amount limit in the configurations, or...
+                    n.getEntries().size() > configuration.get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asInteger() ||
+                            // any of the constants has a comment.
+                            n.getEntries().stream().anyMatch(e -> e.getComment().isPresent());
+            printer.println();
+            for (final Iterator<EnumConstantDeclaration> i = n.getEntries().iterator(); i.hasNext(); ) {
+                final EnumConstantDeclaration e = i.next();
+                e.accept(this, arg);
+                if (i.hasNext()) {
+                    if (alignVertically) {
+                        printer.println(",");
+                    } else {
+                        printer.print(", ");
+                    }
+                }
+            }
+        }
+        if (!n.getMembers().isEmpty()) {
+            printer.println(";");
+            printMembers(n.getMembers(), arg);
+        } else {
+            if (!n.getEntries().isEmpty()) {
+                printer.println();
+            }
+        }
+        printer.unindent();
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final EnumConstantDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        n.getName().accept(this, arg);
+
+        if (!n.getArguments().isEmpty()) {
+            printArguments(n.getArguments(), arg);
+        }
+
+        if (!n.getClassBody().isEmpty()) {
+            printer.println(" {");
+            printer.indent();
+            printMembers(n.getClassBody(), arg);
+            printer.unindent();
+            printer.println("}");
+        }
+    }
+
+    @Override
+    public void visit(final InitializerDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.isStatic()) {
+            printer.print("static ");
+        }
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final IfStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("if (");
+        n.getCondition().accept(this, arg);
+        final boolean thenBlock = n.getThenStmt() instanceof BlockStmt;
+        if (thenBlock) // block statement should start on the same line
+            printer.print(") ");
+        else {
+            printer.println(")");
+            printer.indent();
+        }
+        n.getThenStmt().accept(this, arg);
+        if (!thenBlock)
+            printer.unindent();
+        if (n.getElseStmt().isPresent()) {
+            if (thenBlock)
+                printer.print(" ");
+            else
+                printer.println();
+            final boolean elseIf = n.getElseStmt().orElse(null) instanceof IfStmt;
+            final boolean elseBlock = n.getElseStmt().orElse(null) instanceof BlockStmt;
+            if (elseIf || elseBlock) // put chained if and start of block statement on a same level
+                printer.print("else ");
+            else {
+                printer.println("else");
+                printer.indent();
+            }
+            if (n.getElseStmt().isPresent())
+                n.getElseStmt().get().accept(this, arg);
+            if (!(elseIf || elseBlock))
+                printer.unindent();
+        }
+    }
+
+    @Override
+    public void visit(final WhileStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("while (");
+        n.getCondition().accept(this, arg);
+        printer.print(") ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ContinueStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("continue");
+        n.getLabel().ifPresent(l -> printer.print(" ").print(l.getIdentifier()));
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final DoStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("do ");
+        n.getBody().accept(this, arg);
+        printer.print(" while (");
+        n.getCondition().accept(this, arg);
+        printer.print(");");
+    }
+
+    @Override
+    public void visit(final ForEachStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("for (");
+        n.getVariable().accept(this, arg);
+        printer.print(" : ");
+        n.getIterable().accept(this, arg);
+        printer.print(") ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ForStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("for (");
+        if (n.getInitialization() != null) {
+            for (final Iterator<Expression> i = n.getInitialization().iterator(); i.hasNext(); ) {
+                final Expression e = i.next();
+                e.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print("; ");
+        if (n.getCompare().isPresent()) {
+            n.getCompare().get().accept(this, arg);
+        }
+        printer.print("; ");
+        if (n.getUpdate() != null) {
+            for (final Iterator<Expression> i = n.getUpdate().iterator(); i.hasNext(); ) {
+                final Expression e = i.next();
+                e.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print(") ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final ThrowStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("throw ");
+        n.getExpression().accept(this, arg);
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final SynchronizedStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("synchronized (");
+        n.getExpression().accept(this, arg);
+        printer.print(") ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final TryStmt n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("try ");
+        if (!n.getResources().isEmpty()) {
+            printer.print("(");
+            Iterator<Expression> resources = n.getResources().iterator();
+            boolean first = true;
+            while (resources.hasNext()) {
+                resources.next().accept(this, arg);
+                if (resources.hasNext()) {
+                    printer.print(";");
+                    printer.println();
+                    if (first) {
+                        printer.indent();
+                    }
+                }
+                first = false;
+            }
+            if (n.getResources().size() > 1) {
+                printer.unindent();
+            }
+            printer.print(") ");
+        }
+        n.getTryBlock().accept(this, arg);
+        for (final CatchClause c : n.getCatchClauses()) {
+            c.accept(this, arg);
+        }
+        if (n.getFinallyBlock().isPresent()) {
+            printer.print(" finally ");
+            n.getFinallyBlock().get().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(final CatchClause n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print(" catch (");
+        n.getParameter().accept(this, arg);
+        printer.print(") ");
+        n.getBody().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final AnnotationDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+
+        printer.print("@interface ");
+        n.getName().accept(this, arg);
+        printer.println(" {");
+        printer.indent();
+        if (n.getMembers() != null) {
+            printMembers(n.getMembers(), arg);
+        }
+        printer.unindent();
+        printer.print("}");
+    }
+
+    @Override
+    public void visit(final AnnotationMemberDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printMemberAnnotations(n.getAnnotations(), arg);
+        printModifiers(n.getModifiers());
+
+        n.getType().accept(this, arg);
+        printer.print(" ");
+        n.getName().accept(this, arg);
+        printer.print("()");
+        if (n.getDefaultValue().isPresent()) {
+            printer.print(" default ");
+            n.getDefaultValue().get().accept(this, arg);
+        }
+        printer.print(";");
+    }
+
+    @Override
+    public void visit(final MarkerAnnotationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("@");
+        n.getName().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final SingleMemberAnnotationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("@");
+        n.getName().accept(this, arg);
+        printer.print("(");
+        n.getMemberValue().accept(this, arg);
+        printer.print(")");
+    }
+
+    @Override
+    public void visit(final NormalAnnotationExpr n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("@");
+        n.getName().accept(this, arg);
+        printer.print("(");
+        if (n.getPairs() != null) {
+            for (final Iterator<MemberValuePair> i = n.getPairs().iterator(); i.hasNext(); ) {
+                final MemberValuePair m = i.next();
+                m.accept(this, arg);
+                if (i.hasNext()) {
+                    printer.print(", ");
+                }
+            }
+        }
+        printer.print(")");
+    }
+
+    @Override
+    public void visit(final MemberValuePair n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        n.getName().accept(this, arg);
+        printer.print(" = ");
+        n.getValue().accept(this, arg);
+    }
+
+    @Override
+    public void visit(final LineComment n, final Void arg) {
+        if (!configuration.get(ConfigOption.PRINT_COMMENTS).isPresent()) {
+            return;
+        }
+        printer
+                .print("// ")
+                .println(normalizeEolInTextBlock(n.getContent(), "").trim());
+    }
+
+    @Override
+    public void visit(final BlockComment n, final Void arg) {
+        if (!configuration.get(ConfigOption.PRINT_COMMENTS).isPresent()) {
+            return;
+        }
+        final String commentContent = normalizeEolInTextBlock(n.getContent(), configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asString());
+        String[] lines = commentContent.split("\\R", -1); // as BlockComment should not be formatted, -1 to preserve any trailing empty line if present
+        printer.print("/*");
+        for (int i = 0; i < (lines.length - 1); i++) {
+            printer.print(lines[i]);
+            printer.print(configuration.get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue()); // Avoids introducing indentation in blockcomments. ie: do not use println() as it would trigger indentation at the next print call.
+        }
+        printer.print(lines[lines.length - 1]); // last line is not followed by a newline, and simply terminated with `*/`
+        printer.println("*/");
+    }
+
+    @Override
+    public void visit(LambdaExpr n, Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+
+        final NodeList<Parameter> parameters = n.getParameters();
+        final boolean printPar = n.isEnclosingParameters();
+
+        if (printPar) {
+            printer.print("(");
+        }
+        for (Iterator<Parameter> i = parameters.iterator(); i.hasNext(); ) {
+            Parameter p = i.next();
+            p.accept(this, arg);
+            if (i.hasNext()) {
+                printer.print(", ");
+            }
+        }
+        if (printPar) {
+            printer.print(")");
+        }
+
+        printer.print(" -> ");
+        final Statement body = n.getBody();
+        if (body instanceof ExpressionStmt) {
+            // Print the expression directly
+            ((ExpressionStmt) body).getExpression().accept(this, arg);
+        } else {
+            body.accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(MethodReferenceExpr n, Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        Expression scope = n.getScope();
+        String identifier = n.getIdentifier();
+        if (scope != null) {
+            n.getScope().accept(this, arg);
+        }
+
+        printer.print("::");
+        printTypeArgs(n, arg);
+        if (identifier != null) {
+            printer.print(identifier);
+        }
+    }
+
+    @Override
+    public void visit(TypeExpr n, Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        if (n.getType() != null) {
+            n.getType().accept(this, arg);
+        }
+    }
+
+    @Override
+    public void visit(NodeList n, Void arg) {
+        if (configuration.get(ConfigOption.ORDER_IMPORTS).isPresent() && n.size() > 0 && n.get(0) instanceof ImportDeclaration) {
+            //noinspection unchecked
+            NodeList<ImportDeclaration> modifiableList = new NodeList<>(n);
+            modifiableList.sort(
+                    comparingInt((ImportDeclaration i) -> i.isStatic() ? 0 : 1)
+                            .thenComparing(NodeWithName::getNameAsString));
+            for (Object node : modifiableList) {
+                ((Node) node).accept(this, arg);
+            }
+        } else {
+            for (Object node : n) {
+                ((Node) node).accept(this, arg);
+            }
+        }
+    }
+
+    @Override
+    public void visit(final ImportDeclaration n, final Void arg) {
+        printOrphanCommentsBeforeThisChildNode(n);
+        printComment(n.getComment(), arg);
+        printer.print("import ");
+        if (n.isStatic()) {
+            printer.print("static ");
+        }
+        n.getName().accept(this, arg);
+        if (n.isAsterisk()) {
+            printer.print(".*");
+        }
+        printer.println(";");
+
+        printOrphanCommentsEnding(n);
+    }
+
+
+    @Override
+    public void visit(ModuleDeclaration n, Void arg) {
+        printMemberAnnotations(n.getAnnotations(), arg);
+        if (n.isOpen()) {
+            printer.print("open ");
+        }
+        printer.print("module ");
+        n.getName().accept(this, arg);
+        printer.println(" {").indent();
+        n.getDirectives().accept(this, arg);
+        printer.unindent().println("}");
+    }
+
+    @Override
+    public void visit(ModuleRequiresDirective n, Void arg) {
+        printer.print("requires ");
+        printModifiers(n.getModifiers());
+        n.getName().accept(this, arg);
+        printer.println(";");
+    }
+
+    @Override
+    public void visit(ModuleExportsDirective n, Void arg) {
+        printer.print("exports ");
+        n.getName().accept(this, arg);
+        printPrePostFixOptionalList(n.getModuleNames(), arg, " to ", ", ", "");
+        printer.println(";");
+    }
+
+    @Override
+    public void visit(ModuleProvidesDirective n, Void arg) {
+        printer.print("provides ");
+        n.getName().accept(this, arg);
+        printPrePostFixRequiredList(n.getWith(), arg, " with ", ", ", "");
+        printer.println(";");
+    }
+
+    @Override
+    public void visit(ModuleUsesDirective n, Void arg) {
+        printer.print("uses ");
+        n.getName().accept(this, arg);
+        printer.println(";");
+    }
+
+    @Override
+    public void visit(ModuleOpensDirective n, Void arg) {
+        printer.print("opens ");
+        n.getName().accept(this, arg);
+        printPrePostFixOptionalList(n.getModuleNames(), arg, " to ", ", ", "");
+        printer.println(";");
+    }
+
+    @Override
+    public void visit(UnparsableStmt n, Void arg) {
+        printer.print("???;");
+    }
+
+    private void printOrphanCommentsBeforeThisChildNode(final Node node) {
+        if (!configuration.get(ConfigOption.PRINT_COMMENTS).isPresent()) return;
+        if (node instanceof Comment) return;
+
+        Node parent = node.getParentNode().orElse(null);
+        if (parent == null) return;
+        List<Node> everything = new ArrayList<>(parent.getChildNodes());
+        sortByBeginPosition(everything);
+        int positionOfTheChild = -1;
+        for (int i = 0; i < everything.size(); ++i) { // indexOf is by equality, so this is used to index by identity
+            if (everything.get(i) == node) {
+                positionOfTheChild = i;
+                break;
+            }
+        }
+        if (positionOfTheChild == -1) {
+            throw new AssertionError("I am not a child of my parent.");
+        }
+        int positionOfPreviousChild = -1;
+        for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) {
+            if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i;
+        }
+        for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) {
+            Node nodeToPrint = everything.get(i);
+            if (!(nodeToPrint instanceof Comment))
+                throw new RuntimeException(
+                        "Expected comment, instead " + nodeToPrint.getClass() + ". Position of previous child: "
+                                + positionOfPreviousChild + ", position of child " + positionOfTheChild);
+            nodeToPrint.accept(this, null);
+        }
+    }
+
+    private void printOrphanCommentsEnding(final Node node) {
+        if (!configuration.get(ConfigOption.PRINT_COMMENTS).isPresent()) return;
+
+        // extract all nodes for which the position/range is indicated to avoid to skip orphan comments
+        List<Node> everything = node.getChildNodes().stream().filter(n->n.getRange().isPresent()).collect(Collectors.toList());
+        sortByBeginPosition(everything);
+        if (everything.isEmpty()) {
+            return;
+        }
+
+        int commentsAtEnd = 0;
+        boolean findingComments = true;
+        while (findingComments && commentsAtEnd < everything.size()) {
+            Node last = everything.get(everything.size() - 1 - commentsAtEnd);
+            findingComments = (last instanceof Comment);
+            if (findingComments) {
+                commentsAtEnd++;
+            }
+        }
+        for (int i = 0; i < commentsAtEnd; i++) {
+            everything.get(everything.size() - commentsAtEnd + i).accept(this, null);
+        }
+    }
+     private void indentIf(boolean expr){
+        if(expr)
+            printer.indent();
+     }
+    private void unindentIf(boolean expr){
+        if(expr)
+            printer.unindent();
+    }
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/SourcePrinter.java b/javaparser-core/src/main/java/com/github/javaparser/printer/SourcePrinter.java
index 3bb483a..a66afa2 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/SourcePrinter.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/SourcePrinter.java
@@ -27,12 +27,16 @@
 import com.github.javaparser.Position;
 import com.github.javaparser.printer.configuration.Indentation;
 import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;
 import com.github.javaparser.utils.Utils;
 
 /**
  * A support class for code that outputs formatted source code.
+ * This class is no longer acceptable to use because it is not sufficiently configurable and it is too tied to a specific implementation
+ * <p> Use {@link PrintableSource interface or DefaultPrintableSource default implementation } instead.
  */
-public class SourcePrinter {
+@Deprecated
+public class SourcePrinter{
     private final String endOfLineCharacter;
     private final Indentation indentation;
 
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/ConfigurablePrinter.java b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/ConfigurablePrinter.java
new file mode 100755
index 0000000..69e14d4
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/ConfigurablePrinter.java
@@ -0,0 +1,45 @@
+package com.github.javaparser.printer.configuration;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
+
+/*
+ * This interface defines the api for printer configuration
+ */
+public interface ConfigurablePrinter {
+
+    /*
+     * add or update an option
+     */
+    ConfigurablePrinter addOption(ConfigOption option);
+    
+    /*
+     * Remove an option
+     */
+    ConfigurablePrinter removeOption(ConfigOption option);
+
+    /*
+     * True if an option is activated
+     */
+    boolean isActivated(ConfigOption option);
+
+    /*
+     * returns the specified option
+     */
+    Optional<ConfigOption> get(ConfigOption option);
+
+    /*
+     * returns all activated options
+     */
+    Set<ConfigOption> get();
+
+    /*
+     * returns the indentation parameters
+     */
+    Indentation getIndentation();
+
+    ConfigurablePrinter setIndentation(Indentation indentation);
+
+}
\ No newline at end of file
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinterConfiguration.java b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrettyPrinterConfiguration.java
similarity index 63%
rename from javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinterConfiguration.java
rename to javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrettyPrinterConfiguration.java
index aa3f00b..0e50bbe 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/printer/PrettyPrinterConfiguration.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrettyPrinterConfiguration.java
@@ -19,32 +19,30 @@
  * GNU Lesser General Public License for more details.
  */
 
-package com.github.javaparser.printer;
+package com.github.javaparser.printer.configuration;
 
-import static com.github.javaparser.utils.Utils.SYSTEM_EOL;
 import static com.github.javaparser.utils.Utils.assertNonNegative;
 import static com.github.javaparser.utils.Utils.assertNotNull;
 import static com.github.javaparser.utils.Utils.assertPositive;
 
-import java.util.function.Function;
+import java.util.Optional;
+import java.util.Set;
 
-import com.github.javaparser.ast.visitor.VoidVisitor;
-import com.github.javaparser.printer.configuration.Indentation;
+import com.github.javaparser.printer.PrettyPrinter;
 import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.printer.configuration.PrinterConfiguration.ConfigOption;
 
 /**
  * Configuration options for the {@link PrettyPrinter}.
+ * This class is no longer acceptable to use because it is not sufficiently configurable and it is too tied to a specific implementation
+ * <p> Use {@link ConfigurablePrinter interface or PrinterConfiguration default implementation } instead.
  */
-public class PrettyPrinterConfiguration {
+@Deprecated
+public class PrettyPrinterConfiguration implements ConfigurablePrinter {
     
-    public static final int DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY = 5;
-
-    private boolean orderImports = false;
-    private boolean printComments = true;
-    private boolean printJavadoc = true;
-    private boolean spaceAroundOperators = true;
-    private boolean columnAlignParameters = false;
-    private boolean columnAlignFirstMethodChain = false;
+    
+    ConfigurablePrinter wrapped;
+    
     /**
      * Indent the case when it is true, don't if false
      * switch(x) {            switch(x) {
@@ -54,11 +52,15 @@
      *        return z;           return x;
      *}                       }
      */
-    private boolean indentCaseInSwitch = true;
-    private Indentation indentation = new Indentation(IndentType.SPACES, 4);
-    private String endOfLineCharacter = SYSTEM_EOL;
-    private Function<PrettyPrinterConfiguration, VoidVisitor<Void>> visitorFactory = PrettyPrintVisitor::new;
-    private int maxEnumConstantsToAlignHorizontally = DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY;
+    private Indentation indentation;
+    
+    /*
+     * Default constructor
+     */
+    public PrettyPrinterConfiguration() {
+        this.wrapped = new PrinterConfiguration();
+        this.indentation = new Indentation(IndentType.SPACES, 4);
+    }
     
     /*
      * returns the indentation parameters
@@ -141,31 +143,35 @@
     }
 
     public boolean isOrderImports() {
-        return orderImports;
+        return get(ConfigOption.ORDER_IMPORTS).isPresent();
     }
 
     public boolean isPrintComments() {
-        return printComments;
+        return get(ConfigOption.PRINT_COMMENTS).isPresent();
     }
 
     public boolean isIgnoreComments() {
-        return !printComments;
+        return !get(ConfigOption.PRINT_COMMENTS).isPresent();
     }
     
-    public boolean isSpaceAroundOperators() { return spaceAroundOperators; }
+    public boolean isSpaceAroundOperators() {
+        return get(ConfigOption.SPACE_AROUND_OPERATORS).isPresent();
+    }
 
     public boolean isPrintJavadoc() {
-        return printJavadoc;
+        return get(ConfigOption.PRINT_JAVADOC).isPresent();
     }
 
     public boolean isColumnAlignParameters() {
-        return columnAlignParameters;
+        return get(ConfigOption.COLUMN_ALIGN_PARAMETERS).isPresent();
     }
 
-    public boolean isColumnAlignFirstMethodChain() { return columnAlignFirstMethodChain; }
+    public boolean isColumnAlignFirstMethodChain() {
+        return get(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN).isPresent();
+    }
 
     public boolean isIndentCaseInSwitch() {
-        return indentCaseInSwitch;
+        return get(ConfigOption.INDENT_CASE_IN_SWITCH).isPresent();
     }
 
 
@@ -174,7 +180,7 @@
      * printed.
      */
     public PrettyPrinterConfiguration setPrintComments(boolean printComments) {
-        this.printComments = printComments;
+        wrapped = printComments ? addOption(ConfigOption.PRINT_COMMENTS) : removeOption(ConfigOption.PRINT_COMMENTS);
         return this;
     }
 
@@ -182,7 +188,7 @@
      * When true, Javadoc will be printed.
      */
     public PrettyPrinterConfiguration setPrintJavadoc(boolean printJavadoc) {
-        this.printJavadoc = printJavadoc;
+        wrapped = printJavadoc ? addOption(ConfigOption.PRINT_JAVADOC) : removeOption(ConfigOption.PRINT_JAVADOC);
         return this;
     }
 
@@ -190,47 +196,34 @@
      * Set if there should be spaces between operators
      */
     public PrettyPrinterConfiguration setSpaceAroundOperators(boolean spaceAroundOperators){
-        this.spaceAroundOperators = spaceAroundOperators;
+        wrapped = spaceAroundOperators ? addOption(ConfigOption.SPACE_AROUND_OPERATORS) : removeOption(ConfigOption.SPACE_AROUND_OPERATORS);
         return this;
     }
 
     public PrettyPrinterConfiguration setColumnAlignParameters(boolean columnAlignParameters) {
-        this.columnAlignParameters = columnAlignParameters;
+        wrapped = columnAlignParameters ? addOption(ConfigOption.COLUMN_ALIGN_PARAMETERS) : removeOption(ConfigOption.COLUMN_ALIGN_PARAMETERS);
         return this;
     }
 
     public PrettyPrinterConfiguration setColumnAlignFirstMethodChain(boolean columnAlignFirstMethodChain) {
-        this.columnAlignFirstMethodChain = columnAlignFirstMethodChain;
+        wrapped = columnAlignFirstMethodChain ? addOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN) : removeOption(ConfigOption.COLUMN_ALIGN_FIRST_METHOD_CHAIN);
         return this;
     }
 
     public PrettyPrinterConfiguration setIndentCaseInSwitch(boolean indentInSwitch) {
-        this.indentCaseInSwitch = indentInSwitch;
-        return this;
-    }
-
-    public Function<PrettyPrinterConfiguration, VoidVisitor<Void>> getVisitorFactory() {
-        return visitorFactory;
-    }
-
-    /**
-     * Set the factory that creates the PrettyPrintVisitor. By changing this you can make the PrettyPrinter use a custom
-     * PrettyPrinterVisitor.
-     */
-    public PrettyPrinterConfiguration setVisitorFactory(Function<PrettyPrinterConfiguration, VoidVisitor<Void>> visitorFactory) {
-        this.visitorFactory = assertNotNull(visitorFactory);
+        wrapped = indentInSwitch ? addOption(ConfigOption.INDENT_CASE_IN_SWITCH) : removeOption(ConfigOption.INDENT_CASE_IN_SWITCH);
         return this;
     }
 
     public String getEndOfLineCharacter() {
-        return endOfLineCharacter;
+        return get(ConfigOption.END_OF_LINE_CHARACTER).get().asValue();
     }
 
     /**
      * Set the character to append when a line should end. Example values: "\n", "\r\n", "".
      */
     public PrettyPrinterConfiguration setEndOfLineCharacter(String endOfLineCharacter) {
-        this.endOfLineCharacter = assertNotNull(endOfLineCharacter);
+        addOption(ConfigOption.END_OF_LINE_CHARACTER.value(endOfLineCharacter));
         return this;
     }
 
@@ -238,14 +231,14 @@
      * When true, orders imports by alphabetically.
      */
     public PrettyPrinterConfiguration setOrderImports(boolean orderImports) {
-        this.orderImports = orderImports;
+        wrapped = orderImports ? addOption(ConfigOption.ORDER_IMPORTS) : removeOption(ConfigOption.ORDER_IMPORTS);
         return this;
     }
 
 
 
     public int getMaxEnumConstantsToAlignHorizontally() {
-        return maxEnumConstantsToAlignHorizontally;
+        return get(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY).get().asInteger();
     }
 
     /**
@@ -272,7 +265,32 @@
      * Set it to 1 or less to always align vertically.
      */
     public PrettyPrinterConfiguration setMaxEnumConstantsToAlignHorizontally(int maxEnumConstantsToAlignHorizontally) {
-        this.maxEnumConstantsToAlignHorizontally = maxEnumConstantsToAlignHorizontally;
+        addOption(ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY.value(maxEnumConstantsToAlignHorizontally));
         return this;
     }
+
+    @Override
+    public ConfigurablePrinter addOption(ConfigOption option) {
+        return wrapped.addOption(option);
+    }
+
+    @Override
+    public boolean isActivated(ConfigOption option) {
+        return wrapped.isActivated(option);
+    }
+
+    @Override
+    public Optional<ConfigOption> get(ConfigOption option) {
+        return wrapped.get(option);
+    }
+
+    @Override
+    public Set<ConfigOption> get() {
+        return wrapped.get();
+    }
+
+    @Override
+    public ConfigurablePrinter removeOption(ConfigOption option) {
+        return wrapped.removeOption(option);
+    }
 }
diff --git a/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrinterConfiguration.java b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrinterConfiguration.java
new file mode 100755
index 0000000..36b558d
--- /dev/null
+++ b/javaparser-core/src/main/java/com/github/javaparser/printer/configuration/PrinterConfiguration.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2011, 2013-2020 The JavaParser Team.
+ *
+ * This file is part of JavaParser.
+ *
+ * JavaParser can be used either under the terms of
+ * a) the GNU Lesser General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ * b) the terms of the Apache License
+ *
+ * You should have received a copy of both licenses in LICENCE.LGPL and
+ * LICENCE.APACHE. Please refer to those files for details.
+ *
+ * JavaParser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ */
+
+package com.github.javaparser.printer.configuration;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import com.github.javaparser.printer.Printable;
+import com.github.javaparser.printer.configuration.Indentation.IndentType;
+import com.github.javaparser.utils.Utils;
+
+/**
+ * Configuration options for the {@link Printable}.
+ */
+public class PrinterConfiguration implements ConfigurablePrinter {
+    
+    public enum ConfigOption {
+        ORDER_IMPORTS(Boolean.class), 
+        PRINT_COMMENTS(Boolean.class), 
+        PRINT_JAVADOC(Boolean.class), 
+        SPACE_AROUND_OPERATORS(Boolean.class), 
+        COLUMN_ALIGN_PARAMETERS(Boolean.class), 
+        COLUMN_ALIGN_FIRST_METHOD_CHAIN(Boolean.class),
+        /**
+         * Indent the case when it is true, don't if false
+         * switch(x) {            switch(x) {
+         *    case 1:             case 1:
+         *        return y;           return y;
+         *    case 2:             case 2:
+         *        return z;           return x;
+         *}                       }
+         */
+        INDENT_CASE_IN_SWITCH(Boolean.class),
+        /**
+         * By default enum constants get aligned like this:
+         * <pre>
+         *     enum X {
+         *        A, B, C, D
+         *     }
+         * </pre>
+         * until the amount of constants passes this value (5 by default).
+         * Then they get aligned like this:
+         * <pre>
+         *     enum X {
+         *        A,
+         *        B,
+         *        C,
+         *        D,
+         *        E,
+         *        F,
+         *        G
+         *     }
+         * </pre>
+         * Set it to a large number to always align horizontally.
+         * Set it to 1 or less to always align vertically.
+         */
+        DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY(Integer.valueOf(5)),
+        END_OF_LINE_CHARACTER(Utils.SYSTEM_EOL);
+        
+        Object value;
+        
+        Class type;
+        
+        // Option without value
+        <T> ConfigOption(Class clazz) {
+            this.type = clazz;
+        }
+        
+        // Option with initial value
+        ConfigOption(Object value) {
+            value(value);
+        }
+        
+       /* 
+        * Set a value to an option
+        */
+        public ConfigOption value(Object value) {
+            Utils.assertNotNull(value);
+            this.value = value;
+            this.type = value.getClass();
+            return this;
+        }
+        
+        /*
+         * returns True if the option has a value
+         */
+        public boolean hasValue() {
+            return value != null;
+        }
+        
+        /*
+         * returns the value as an Integer
+         */
+        public Integer asInteger() {
+            return cast(); 
+        }
+        
+        /*
+         * returns the value as a String
+         */
+        public String asString() {
+            return cast(); 
+        }
+        
+        /*
+         * returns the value as a Boolean
+         */
+        public Boolean asBoolean() {
+            return cast(); 
+        }
+        
+        public <T extends Object> T asValue() {
+            return cast(); 
+        }
+        
+        private <T extends Object> T cast() {
+            if (!hasValue()) throw new IllegalArgumentException(String.format("The option %s has no value", this.name()));
+            if (type.isAssignableFrom(value.getClass()))
+                return (T) type.cast(value);
+            throw new IllegalArgumentException(String.format("%s cannot be cast to %s", value, type.getName()));
+        }
+    }
+
+    // contains all available options
+    // an option contained in the set is considered as activated
+    private Set<ConfigOption> options = new HashSet<ConfigOption>(Arrays.asList(
+            ConfigOption.PRINT_COMMENTS, 
+            ConfigOption.PRINT_JAVADOC, 
+            ConfigOption.SPACE_AROUND_OPERATORS,
+            ConfigOption.INDENT_CASE_IN_SWITCH,
+            ConfigOption.DEFAULT_MAX_ENUM_CONSTANTS_TO_ALIGN_HORIZONTALLY.value(Integer.valueOf(5)),
+            ConfigOption.END_OF_LINE_CHARACTER.value(Utils.SYSTEM_EOL)
+            ));
+
+    private Indentation indentation = new Indentation(IndentType.SPACES, 4);
+    
+    //    private Function<PrinterConfiguration, VoidVisitor<Void>> visitorFactory = PrettyPrintVisitor::new;
+    
+    /*
+     * add or update an option
+     */
+    @Override
+    public ConfigurablePrinter addOption(ConfigOption option) {
+        removeOption(option);
+        options.add(option);
+        return this;
+    }
+    
+    /*
+     * add or update an option
+     */
+    @Override
+    public ConfigurablePrinter removeOption(ConfigOption option) {
+        options.remove(option);
+        return this;
+    }
+    
+    /*
+     * True if an option is activated
+     */
+    @Override
+    public boolean isActivated(ConfigOption option) {
+        return options.contains(option);
+    }
+    
+    /*
+     * returns the specified option
+     */
+    @Override
+    public Optional<ConfigOption> get(ConfigOption option) {
+        return options.stream().filter(o-> o.equals(option)).findFirst();
+    }
+    
+    /*
+     * returns all activated options
+     */
+    @Override
+    public Set<ConfigOption> get() {
+        return options;
+    }
+    
+    /*
+     * returns the indentation parameters
+     */
+    @Override
+    public Indentation getIndentation() {
+        return indentation;
+    }
+    
+    @Override
+    public ConfigurablePrinter setIndentation(Indentation indentation) {
+        this.indentation = indentation;
+        return this;
+    }
+
+}
diff --git a/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java b/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java
index 3a0224b..4c3a277 100644
--- a/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java
+++ b/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java
@@ -21,12 +21,15 @@
 
 package com.github.javaparser.utils;
 
-import com.github.javaparser.JavaParser;
-import com.github.javaparser.ParseProblemException;
-import com.github.javaparser.ParseResult;
-import com.github.javaparser.ParserConfiguration;
-import com.github.javaparser.ast.CompilationUnit;
-import com.github.javaparser.printer.PrettyPrinter;
+import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
+import static com.github.javaparser.Providers.provider;
+import static com.github.javaparser.utils.CodeGenerationUtils.fileInPackageAbsolutePath;
+import static com.github.javaparser.utils.CodeGenerationUtils.fileInPackageRelativePath;
+import static com.github.javaparser.utils.CodeGenerationUtils.packageAbsolutePath;
+import static com.github.javaparser.utils.Utils.assertNotNull;
+import static java.nio.file.FileVisitResult.CONTINUE;
+import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
+import static java.nio.file.FileVisitResult.TERMINATE;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -45,11 +48,12 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
-import static com.github.javaparser.Providers.provider;
-import static com.github.javaparser.utils.CodeGenerationUtils.*;
-import static com.github.javaparser.utils.Utils.assertNotNull;
-import static java.nio.file.FileVisitResult.*;
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParseProblemException;
+import com.github.javaparser.ParseResult;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.printer.PrettyPrintable;
 
 /**
  * A collection of Java source files located in one directory and its subdirectories on the file system. The root directory
@@ -79,7 +83,7 @@
     private final Path root;
     private final Map<Path, ParseResult<CompilationUnit>> cache = new ConcurrentHashMap<>();
     private ParserConfiguration parserConfiguration = new ParserConfiguration();
-    private Function<CompilationUnit, String> printer = new PrettyPrinter()::print;
+    private Function<CompilationUnit, String> printer = new PrettyPrintable()::print;
     private static final Pattern JAVA_IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");
 
     /**
diff --git a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue2764Test.java b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue2764Test.java
index c2c4c44..472aa21 100755
--- a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue2764Test.java
+++ b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue2764Test.java
@@ -10,7 +10,7 @@
 import com.github.javaparser.ast.expr.UnaryExpr;

 import com.github.javaparser.ast.stmt.Statement;

 import com.github.javaparser.printer.PrettyPrinter;

-import com.github.javaparser.printer.PrettyPrinterConfiguration;

+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;

 import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration;

 import com.github.javaparser.symbolsolver.model.resolution.TypeSolver;

 import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;

diff --git a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue546Test.java b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue546Test.java
index 39c5c94..56ce99b 100644
--- a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue546Test.java
+++ b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/Issue546Test.java
@@ -2,7 +2,7 @@
 

 import com.github.javaparser.ast.stmt.Statement;

 import com.github.javaparser.printer.PrettyPrinter;

-import com.github.javaparser.printer.PrettyPrinterConfiguration;

+import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;

 import com.github.javaparser.utils.LineSeparator;

 import org.junit.jupiter.api.Test;