XMLPrinter.java

/*
 *  Copyright (c) 2001-2024, Jean Tessier
 *  All rights reserved.
 *  
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *  
 *      * Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *  
 *      * Neither the name of Jean Tessier nor the names of his contributors
 *        may be used to endorse or promote products derived from this software
 *        without specific prior written permission.
 *  
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jeantessier.classreader;

import com.jeantessier.text.Hex;

import java.io.*;
import java.util.*;

public class XMLPrinter extends Printer {
    public static final String DEFAULT_ENCODING   = "utf-8";
    public static final String DEFAULT_DTD_PREFIX = "https://jeantessier.github.io/dependency-finder/dtd";

    private static final BitFormat format = new BitFormat(16);

    private boolean top = true;

    public XMLPrinter(PrintWriter out) {
        this(out, DEFAULT_ENCODING, DEFAULT_DTD_PREFIX);
    }
    
    public XMLPrinter(PrintWriter out, String encoding, String dtdPrefix) {
        super(out);
        
        appendHeader(encoding, dtdPrefix);
    }

    private Printer appendHeader(String encoding, String dtdPrefix) {
        append("<?xml version=\"1.0\" encoding=\"").append(encoding).append("\" ?>").eol();
        eol();
        append("<!DOCTYPE classfiles SYSTEM \"").append(dtdPrefix).append("/classfile.dtd\">").eol();
        eol();

        return this;
    }

    public void visitClassfiles(Collection<Classfile> classfiles) {
        indent().append("<classfiles>").eol();
        raiseIndent();

        super.visitClassfiles(classfiles);

        lowerIndent();
        indent().append("</classfiles>").eol();
    }
    
    public void visitClassfile(Classfile classfile) {
        indent().append("<!-- ").append(classfile.getClassName()).append(" -->").eol();
        indent().append("<classfile magic-number=\"0x").append(Integer.toHexString(classfile.getMagicNumber()).toUpperCase()).append("\" minor-version=\"").append(classfile.getMinorVersion()).append("\" major-version=\"").append(classfile.getMajorVersion()).append("\" access-flags=\"").append(format.format(classfile.getAccessFlags())).append("\">").eol();
        raiseIndent();

        top = true;
        classfile.getConstantPool().accept(this);
        top = false;
        
        if (classfile.isPublic())     indent().append("<public/>").eol();
        if (classfile.isFinal())      indent().append("<final/>").eol();
        if (classfile.isSuper())      indent().append("<super/>").eol();
        if (classfile.isInterface())  indent().append("<is-interface/>").eol();
        if (classfile.isAbstract())   indent().append("<abstract/>").eol();
        if (classfile.isSynthetic())  indent().append("<synthetic/>").eol();
        if (classfile.isAnnotation()) indent().append("<is-annotation/>").eol();
        if (classfile.isEnum())       indent().append("<enum/>").eol();
        if (classfile.isModule())     indent().append("<is-module/>").eol();

        indent();
        append("<this-class>");
        classfile.getRawClass().accept(this);
        append("</this-class>").eol();

        indent();
        append("<superclass>");
        if (classfile.hasSuperclass()) {
            classfile.getRawSuperclass().accept(this);
        }
        append("</superclass>").eol();

        if (!classfile.getAllInterfaces().isEmpty()) {
            indent().append("<interfaces>").eol();
            raiseIndent();
            for (Class_info class_info : classfile.getAllInterfaces()) {
                indent();
                append("<interface>");
                class_info.accept(this);
                append("</interface>").eol();
            }
            lowerIndent();
            indent().append("</interfaces>").eol();
        }
        
        if (!classfile.getAllFields().isEmpty()) {
            indent().append("<fields>").eol();
            raiseIndent();

            visitClassfileFields(classfile);

            lowerIndent();
            indent().append("</fields>").eol();
        }

        if (!classfile.getAllMethods().isEmpty()) {
            indent().append("<methods>").eol();
            raiseIndent();

            visitClassfileMethods(classfile);

            lowerIndent();
            indent().append("</methods>").eol();
        }

        if (!classfile.getAttributes().isEmpty()) {
            indent().append("<attributes>").eol();
            raiseIndent();

            visitClassfileAttributes(classfile);

            lowerIndent();
            indent().append("</attributes>").eol();
        }

        lowerIndent();
        indent().append("</classfile>").eol();
    }

    public void visitConstantPool(ConstantPool constantPool) {
        indent().append("<constant-pool>").eol();
        raiseIndent();

        super.visitConstantPool(constantPool);

        lowerIndent();
        indent().append("</constant-pool>").eol();
    }

    public void visitClass_info(Class_info entry) {
        if (top) {
            top = false;
            appendClassInfo(currentIndex(), entry);
            top = true;
        } else {
            // entry.getRawName().accept(this);
            append(entry.getName());
        }
    }

    public void visitFieldRef_info(FieldRef_info entry) {
        Class_info       c   = entry.getRawClass();
        NameAndType_info nat = entry.getRawNameAndType();

        if (top) {
            top = false;
            indent();
            append("<field-ref-info index=\"").append(currentIndex()).append("\">");
            append("<class>");
            c.accept(this);
            append("</class>");
            append("<type>");
            nat.getRawType().accept(this);
            append("</type>");
            append("<name>");
            nat.getRawName().accept(this);
            append("</name>");
            append("</field-ref-info>").eol();
            top = true;
        } else {
            append(DescriptorHelper.getType(nat.getType()));
            append(" ");
            append(entry.getFullSignature());
        }
    }

    public void visitMethodRef_info(MethodRef_info entry) {
        Class_info       c   = entry.getRawClass();
        NameAndType_info nat = entry.getRawNameAndType();

        if (top) {
            top = false;
            indent();
            append("<method-ref-info index=\"").append(currentIndex()).append("\">");
            append("<class>");
            c.accept(this);
            append("</class>");
            append("<name>");
            nat.getRawName().accept(this);
            append("</name>");
            append("<type>");
            nat.getRawType().accept(this);
            append("</type>");
            append("</method-ref-info>").eol();
            top = true;
        } else {
            if (!entry.isConstructor() && !entry.isStaticInitializer()) {
                append(DescriptorHelper.getReturnType(nat.getType())).append(" ");
            }
            append(entry.getFullSignature());
        }
    }

    public void visitInterfaceMethodRef_info(InterfaceMethodRef_info entry) {
        Class_info       c   = entry.getRawClass();
        NameAndType_info nat = entry.getRawNameAndType();

        if (top) {
            top = false;
            indent();
            append("<interface-method-ref-info index=\"").append(currentIndex()).append("\">");
            append("<class>");
            c.accept(this);
            append("</class>");
            append("<name>");
            nat.getRawName().accept(this);
            append("</name>");
            append("<type>");
            nat.getRawType().accept(this);
            append("</type>");
            append("</interface-method-ref-info>").eol();
            top = true;
        } else {
            append(DescriptorHelper.getReturnType(nat.getType()));
            append(" ");
            append(entry.getFullSignature());
        }
    }

    public void visitString_info(String_info entry) {
        if (top) {
            top = false;
            indent();
            append("<string-info index=\"").append(currentIndex()).append("\">");
            entry.getRawValue().accept(this);
            append("</string-info>").eol();
            top = true;
        } else {
            entry.getRawValue().accept(this);
        }
    }

    public void visitInteger_info(Integer_info entry) {
        if (top) {
            top = false;
            indent();
            append("<integer-info index=\"").append(currentIndex()).append("\">");
            append(entry.getValue());
            append("</integer-info>").eol();
            top = true;
        } else {
            append(entry.getValue());
        }
    }

    public void visitFloat_info(Float_info entry) {
        if (top) {
            top = false;
            indent();
            append("<float-info index=\"").append(currentIndex()).append("\">");
            append(entry.getValue());
            append("</float-info>").eol();
            top = true;
        } else {
            append(entry.getValue());
        }
    }

    public void visitLong_info(Long_info entry) {
        if (top) {
            top = false;
            indent();
            append("<long-info index=\"").append(currentIndex()).append("\">");
            append(entry.getValue());
            append("</long-info>").eol();
            top = true;
        } else {
            append(entry.getValue());
        }
    }

    public void visitDouble_info(Double_info entry) {
        if (top) {
            top = false;
            indent();
            append("<double-info index=\"").append(currentIndex()).append("\">");
            append(entry.getValue());
            append("</double-info>").eol();
            top = true;
        } else {
            append(entry.getValue());
        }
    }

    public void visitNameAndType_info(NameAndType_info entry) {
        if (top) {
            top = false;
            indent();
            append("<name-and-type-info index=\"").append(currentIndex()).append("\">");
            append("<name>");
            entry.getRawName().accept(this);
            append("</name>");
            append("<type>");
            entry.getRawType().accept(this);
            append("</type>");
            append("</name-and-type-info>").eol();
            top = true;
        } else {
            entry.getRawName().accept(this);
            append(" ");
            entry.getRawType().accept(this);
        }
    }

    public void visitUTF8_info(UTF8_info entry) {
        if (top) {
            top = false;
            indent().append("<utf8-info index=\"").append(currentIndex()).append("\">");
            append(escapeXMLCharacters(entry.getValue()));
            append("</utf8-info>").eol();
            top = true;
        } else {
            append(escapeXMLCharacters(entry.getValue()));
        }
    }

    public void visitMethodHandle_info(MethodHandle_info entry) {
        if (top) {
            top = false;
            indent();
            append("<method-handle-info index=\"").append(currentIndex()).append("\">");
            append("<reference-kind kind=\"").append(entry.getRawReferenceKind()).append("\">");
            append(entry.getReferenceKind().getDescription());
            append("</reference-kind>");
            append("<reference index=\"").append(entry.getReferenceIndex()).append("\">");
            entry.getReference().accept(this);
            append("</reference>");
            append("</method-handle-info>").eol();
            top = true;
        } else {
            append(entry.getReferenceKind().getDescription());
            append(" ");
            entry.getReference().accept(this);
        }
    }

    public void visitMethodType_info(MethodType_info entry) {
        if (top) {
            top = false;
            indent();
            append("<method-type-info index=\"").append(currentIndex()).append("\">");
            entry.getRawDescriptor().accept(this);
            append("</method-type-info>").eol();
            top = true;
        } else {
            entry.getRawDescriptor().accept(this);
        }
    }

    public void visitDynamic_info(Dynamic_info entry) {
        NameAndType_info nat = entry.getRawNameAndType();

        if (top) {
            top = false;
            indent();
            append("<dynamic-info index=\"").append(currentIndex()).append("\">");
            append("<bootstrap-method-attr index=\"").append(entry.getBootstrapMethodAttrIndex()).append("\"/>");
            append("<name>");
            nat.getRawName().accept(this);
            append("</name>");
            append("<type>");
            nat.getRawType().accept(this);
            append("</type>");
            append("</dynamic-info>").eol();
            top = true;
        } else {
            if (!entry.isStaticInitializer()) {
                append(DescriptorHelper.getReturnType(nat.getType())).append(" ");
            }
            append(entry.getSignature());
        }
    }

    public void visitInvokeDynamic_info(InvokeDynamic_info entry) {
        NameAndType_info nat = entry.getRawNameAndType();

        if (top) {
            top = false;
            indent();
            append("<invoke-dynamic-info index=\"").append(currentIndex()).append("\">");
            append("<bootstrap-method-attr index=\"").append(entry.getBootstrapMethodAttrIndex()).append("\"/>");
            append("<name>");
            nat.getRawName().accept(this);
            append("</name>");
            append("<type>");
            nat.getRawType().accept(this);
            append("</type>");
            append("</invoke-dynamic-info>").eol();
            top = true;
        } else {
            if (!entry.isStaticInitializer()) {
                append(DescriptorHelper.getReturnType(nat.getType())).append(" ");
            }
            append(entry.getSignature());
        }
    }

    public void visitModule_info(Module_info entry) {
        if (top) {
            top = false;
            indent();
            append("<module index=\"").append(currentIndex()).append("\">");
            // entry.getRawName().accept(this);
            append(entry.getName());
            append("</module>").eol();
            top = true;
        } else {
            // entry.getRawName().accept(this);
            append(entry.getName());
        }
    }

    public void visitPackage_info(Package_info entry) {
        if (top) {
            top = false;
            indent();
            append("<package index=\"").append(currentIndex()).append("\">");
            // entry.getRawName().accept(this);
            append(entry.getName());
            append("</package>").eol();
            top = true;
        } else {
            // entry.getRawName().accept(this);
            append(entry.getName());
        }
    }

    public void visitUnusableEntry(UnusableEntry entry) {
        if (top) {
            top = false;
            indent().append("<unusable index=\"").append(currentIndex()).append("\">").append(entry.getReason()).append("</unusable>").eol();
            top = true;
        } else {
            append(entry);
        }
    }

    public void visitField_info(Field_info entry) {
        indent().append("<field-info access-flags=\"").append(format.format(entry.getAccessFlags())).append("\">").eol();
        raiseIndent();

        if (entry.isPublic())    indent().append("<public/>").eol();
        if (entry.isProtected()) indent().append("<protected/>").eol();
        if (entry.isPrivate())   indent().append("<private/>").eol();
        if (entry.isStatic())    indent().append("<static/>").eol();
        if (entry.isFinal())     indent().append("<final/>").eol();
        if (entry.isVolatile())  indent().append("<volatile/>").eol();
        if (entry.isTransient()) indent().append("<transient/>").eol();
        if (entry.isSynthetic()) indent().append("<synthetic/>").eol();
        if (entry.isEnum())      indent().append("<enum/>").eol();

        indent();
        append("<name>");
        entry.getRawName().accept(this);
        append("</name>").eol();
        
        indent().append("<type>").append(entry.getType()).append("</type>").eol();

        if (!entry.getAttributes().isEmpty()) {
            indent().append("<attributes>").eol();
            raiseIndent();
            super.visitField_info(entry);
            lowerIndent();
            indent().append("</attributes>").eol();
        }

        lowerIndent();
        indent().append("</field-info>").eol();
    }

    public void visitMethod_info(Method_info entry) {
        indent().append("<method-info access-flags=\"").append(format.format(entry.getAccessFlags())).append("\">").eol();
        raiseIndent();

        if (entry.isPublic())       indent().append("<public/>").eol();
        if (entry.isProtected())    indent().append("<protected/>").eol();
        if (entry.isPrivate())      indent().append("<private/>").eol();
        if (entry.isStatic())       indent().append("<static/>").eol();
        if (entry.isFinal())        indent().append("<final/>").eol();
        if (entry.isSynchronized()) indent().append("<synchronized/>").eol();
        if (entry.isBridge())       indent().append("<bridge/>").eol();
        if (entry.isVarargs())      indent().append("<varargs/>").eol();
        if (entry.isNative())       indent().append("<native/>").eol();
        if (entry.isAbstract())     indent().append("<abstract/>").eol();
        if (entry.isStrict())       indent().append("<strict/>").eol();
        if (entry.isSynthetic())    indent().append("<synthetic/>").eol();

        indent();
        append("<name>");
        entry.getRawName().accept(this);
        append("</name>").eol();
        
        if (!entry.getName().equals("<init>") && !entry.getName().equals("<clinit>")) {
            indent().append("<return-type>").append((entry.getReturnType() != null) ? entry.getReturnType() : "void").append("</return-type>").eol();
        }
        indent().append("<signature>").append(entry.getSignature()).append("</signature>").eol();

        if (!entry.getAttributes().isEmpty()) {
            indent().append("<attributes>").eol();
            raiseIndent();
            super.visitMethod_info(entry);
            lowerIndent();
            indent().append("</attributes>").eol();
        }

        lowerIndent();
        indent().append("</method-info>").eol();
    }

    public void visitConstantValue_attribute(ConstantValue_attribute attribute) {
        indent().append("<constant-value-attribute>");

        attribute.getRawValue().accept(this);

        append("</constant-value-attribute>").eol();
    }

    public void visitCode_attribute(Code_attribute attribute) {
        indent().append("<code-attribute>").eol();
        raiseIndent();

        indent().append("<length>").append(attribute.getCode().length).append("</length>").eol();

        indent().append("<instructions>").eol();
        raiseIndent();
        for (Instruction instruction : attribute) {
            instruction.accept(this);
        }
        lowerIndent();
        indent().append("</instructions>").eol();

        if (!attribute.getExceptionHandlers().isEmpty()) {
            indent().append("<exception-handlers>").eol();
            raiseIndent();
            for (ExceptionHandler exceptionHandler : attribute.getExceptionHandlers()) {
                exceptionHandler.accept(this);
            }
            lowerIndent();
            indent().append("</exception-handlers>").eol();
        }
        
        if (!attribute.getAttributes().isEmpty()) {
            indent().append("<attributes>").eol();
            raiseIndent();
            for (Attribute_info attribute_info : attribute.getAttributes()) {
                attribute_info.accept(this);
            }
            lowerIndent();
            indent().append("</attributes>").eol();
        }
        
        lowerIndent();
        indent().append("</code-attribute>").eol();
    }

    public void visitStackMapTable_attribute(StackMapTable_attribute attribute) {
        indent().append("<stack-map-table-attribute>").eol();
        raiseIndent();

        super.visitStackMapTable_attribute(attribute);

        lowerIndent();
        indent().append("</stack-map-table-attribute>").eol();
    }

    public void visitExceptions_attribute(Exceptions_attribute attribute) {
        indent().append("<exceptions-attribute>").eol();
        raiseIndent();

        attribute.getExceptions().forEach(exception -> {
            indent();
            append("<exception>");
            exception.accept(this);
            append("</exception>").eol();
        });

        lowerIndent();
        indent().append("</exceptions-attribute>").eol();
    }

    public void visitInnerClasses_attribute(InnerClasses_attribute attribute) {
        indent().append("<inner-classes-attribute>").eol();
        raiseIndent();

        super.visitInnerClasses_attribute(attribute);

        lowerIndent();
        indent().append("</inner-classes-attribute>").eol();
    }

    public void visitEnclosingMethod_attribute(EnclosingMethod_attribute attribute) {
        indent().append("<enclosing-method-attribute>").eol();
        raiseIndent();

        appendClassInfo(attribute.getClassIndex(), attribute.getRawClassInfo());

        indent().append("<method>");
        if (attribute.hasMethod()) {
            NameAndType_info nat = attribute.getRawMethod();
            if (nat.getName().equals("<init>")) {
                String className = attribute.getClassInfo();
                className = className.substring(className.lastIndexOf(".") + 1);
                append(className).append(DescriptorHelper.getSignature(nat.getType()));
            } else {
                append(DescriptorHelper.getReturnType(nat.getType())).append(" ").append(nat.getName()).append(DescriptorHelper.getSignature(nat.getType()));
            }
        }
        append("</method>").eol();

        lowerIndent();
        indent().append("</enclosing-method-attribute>").eol();
    }

    public void visitSynthetic_attribute(Synthetic_attribute attribute) {
        indent().append("<synthetic-attribute/>").eol();
    }

    public void visitSignature_attribute(Signature_attribute attribute) {
        indent().append("<signature-attribute>");
        attribute.getRawSignature().accept(this);
        append("</signature-attribute>").eol();
    }

    public void visitSourceFile_attribute(SourceFile_attribute attribute) {
        indent().append("<source-file-attribute>").append(attribute.getSourceFile()).append("</source-file-attribute>").eol();
    }

    public void visitSourceDebugExtension_attribute(SourceDebugExtension_attribute attribute) {
        indent().append("<source-debug-extension>").append(attribute.getDebugExtension()).append("</source-debug-extension>").eol();
    }

    public void visitLineNumberTable_attribute(LineNumberTable_attribute attribute) {
        indent().append("<line-number-table-attribute>").eol();
        raiseIndent();

        super.visitLineNumberTable_attribute(attribute);

        lowerIndent();
        indent().append("</line-number-table-attribute>").eol();
    }

    public void visitLocalVariableTable_attribute(LocalVariableTable_attribute attribute) {
        indent().append("<local-variable-table-attribute>").eol();
        raiseIndent();

        super.visitLocalVariableTable_attribute(attribute);

        lowerIndent();
        indent().append("</local-variable-table-attribute>").eol();
    }

    public void visitLocalVariableTypeTable_attribute(LocalVariableTypeTable_attribute attribute) {
        indent().append("<local-variable-type-table-attribute>").eol();
        raiseIndent();

        super.visitLocalVariableTypeTable_attribute(attribute);

        lowerIndent();
        indent().append("</local-variable-type-table-attribute>").eol();
    }

    public void visitDeprecated_attribute(Deprecated_attribute attribute) {
        indent().append("<deprecated-attribute/>").eol();
    }

    public void visitRuntimeVisibleAnnotations_attribute(RuntimeVisibleAnnotations_attribute attribute) {
        indent().append("<runtime-visible-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeVisibleAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-visible-annotations-attribute>").eol();
    }

    public void visitRuntimeInvisibleAnnotations_attribute(RuntimeInvisibleAnnotations_attribute attribute) {
        indent().append("<runtime-invisible-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeInvisibleAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-invisible-annotations-attribute>").eol();
    }

    protected void visitRuntimeAnnotations_attribute(RuntimeAnnotations_attribute attribute) {
        indent().append("<annotations>").eol();
        raiseIndent();

        super.visitRuntimeAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</annotations>").eol();
    }

    public void visitRuntimeVisibleParameterAnnotations_attribute(RuntimeVisibleParameterAnnotations_attribute attribute) {
        indent().append("<runtime-visible-parameter-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeVisibleParameterAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-visible-parameter-annotations-attribute>").eol();
    }

    public void visitRuntimeInvisibleParameterAnnotations_attribute(RuntimeInvisibleParameterAnnotations_attribute attribute) {
        indent().append("<runtime-invisible-parameter-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeInvisibleParameterAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-invisible-parameter-annotations-attribute>").eol();
    }

    protected void visitRuntimeParameterAnnotations_attribute(RuntimeParameterAnnotations_attribute attribute) {
        indent().append("<parameter-annotations>").eol();
        raiseIndent();

        super.visitRuntimeParameterAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</parameter-annotations>").eol();
    }

    public void visitRuntimeVisibleTypeAnnotations_attribute(RuntimeVisibleTypeAnnotations_attribute attribute) {
        indent().append("<runtime-visible-type-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeVisibleTypeAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-visible-type-annotations-attribute>").eol();
    }

    public void visitRuntimeInvisibleTypeAnnotations_attribute(RuntimeInvisibleTypeAnnotations_attribute attribute) {
        indent().append("<runtime-invisible-type-annotations-attribute>").eol();
        raiseIndent();

        super.visitRuntimeInvisibleTypeAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</runtime-invisible-type-annotations-attribute>").eol();
    }

    protected void visitRuntimeTypeAnnotations_attribute(RuntimeTypeAnnotations_attribute attribute) {
        indent().append("<type-annotations>").eol();
        raiseIndent();

        super.visitRuntimeTypeAnnotations_attribute(attribute);

        lowerIndent();
        indent().append("</type-annotations>").eol();
    }

    public void visitAnnotationDefault_attribute(AnnotationDefault_attribute attribute) {
        indent().append("<annotation-default-attribute>").eol();
        raiseIndent();

        super.visitAnnotationDefault_attribute(attribute);

        lowerIndent();
        indent().append("</annotation-default-attribute>").eol();
    }

    public void visitBootstrapMethods_attribute(BootstrapMethods_attribute attribute) {
        indent().append("<bootstrap-methods-attribute>").eol();
        raiseIndent();

        super.visitBootstrapMethods_attribute(attribute);

        lowerIndent();
        indent().append("</bootstrap-methods-attribute>").eol();
    }

    public void visitMethodParameters_attribute(MethodParameters_attribute attribute) {
        indent().append("<method-parameters-attribute>").eol();
        raiseIndent();

        super.visitMethodParameters_attribute(attribute);

        lowerIndent();
        indent().append("</method-parameters-attribute>").eol();
    }

    public void visitModule_attribute(Module_attribute attribute) {
        indent().append("<module-attribute module-flags=\"").append(format.format(attribute.getModuleFlags())).append("\">").eol();
        raiseIndent();

        indent();
        append("<name index=\"").append(attribute.getModuleNameIndex()).append("\">");
        attribute.getRawModuleName().accept(this);
        append("</name>").eol();

        if (attribute.isOpen())      indent().append("<open/>").eol();
        if (attribute.isSynthetic()) indent().append("<synthetic/>").eol();
        if (attribute.isMandated())  indent().append("<mandated/>").eol();

        if (attribute.hasModuleVersion()) {
            indent();
            append("<version index=\"").append(attribute.getModuleVersionIndex()).append("\">");
            attribute.getRawModuleVersion().accept(this);
            append("</version>").eol();
        }

        super.visitModule_attribute(attribute);

        lowerIndent();
        indent().append("</module-attribute>").eol();
    }

    public void visitModulePackages_attribute(ModulePackages_attribute attribute) {
        indent().append("<module-packages-attribute>").eol();
        raiseIndent();

        super.visitModulePackages_attribute(attribute);

        lowerIndent();
        indent().append("</module-packages-attribute>").eol();
    }

    public void visitModuleMainClass_attribute(ModuleMainClass_attribute attribute) {
        indent();
        append("<module-main-class-attribute index=\"").append(attribute.getMainClassIndex()).append("\">");
        attribute.getRawMainClass().accept(this);
        append("</module-main-class-attribute>").eol();
    }

    public void visitNestHost_attribute(NestHost_attribute attribute) {
        indent();
        append("<nest-host-attribute index=\"").append(attribute.getHostClassIndex()).append("\">");
        attribute.getRawHostClass().accept(this);
        append("</nest-host-attribute>").eol();
    }

    public void visitNestMembers_attribute(NestMembers_attribute attribute) {
        indent().append("<nest-members-attribute>").eol();
        raiseIndent();

        super.visitNestMembers_attribute(attribute);

        lowerIndent();
        indent().append("</nest-members-attribute>").eol();
    }

    public void visitRecord_attribute(Record_attribute attribute) {
        indent().append("<record-attribute>").eol();
        raiseIndent();

        super.visitRecord_attribute(attribute);

        lowerIndent();
        indent().append("</record-attribute>").eol();
    }

    public void visitPermittedSubclasses_attribute(PermittedSubclasses_attribute attribute) {
        indent().append("<permitted-subclasses-attribute>").eol();
        raiseIndent();

        super.visitPermittedSubclasses_attribute(attribute);

        lowerIndent();
        indent().append("</permitted-subclasses-attribute>").eol();
    }

    public void visitCustom_attribute(Custom_attribute attribute) {
        indent().append("<custom-attribute name=\"").append(escapeXMLCharacters(attribute.getName())).append("\">").append(Hex.toString(attribute.getInfo())).append("</custom-attribute>").eol();
    }

    public void visitInstruction(Instruction instruction) {
        indent();
        append("<instruction pc=\"").append(instruction.getStart()).append("\" length=\"").append(instruction.getLength()).append("\" op-code=\"0x").append(String.format("%02X", instruction.getOpcode())).append("\"");
        switch (instruction.getOpcode()) {
            case 0x02: // iconst_m1
            case 0x03: // iconst_0
            case 0x04: // iconst_1
            case 0x05: // iconst_2
            case 0x06: // iconst_3
            case 0x07: // iconst_4
            case 0x08: // iconst_5
            case 0x09: // lconst_0
            case 0x0a: // lconst_1
            case 0x0b: // fconst_0
            case 0x0c: // fconst_1
            case 0x0d: // fconst_2
            case 0x0e: // dconst_0
            case 0x0f: // dconst_1
                append(" value=\"").append(instruction.getValue()).append("\">");
                append(instruction);
                break;
            case 0x10: // bipush
            case 0x11: // sipush
                append(" value=\"").append(instruction.getValue()).append("\">");
                append(instruction).append(" ").append(instruction.getValue());
                break;
            case 0x12: // ldc
            case 0x13: // ldc_w
            case 0x14: // ldc2_w
            case 0xb2: // getstatic
            case 0xb3: // putstatic
            case 0xb4: // getfield
            case 0xb5: // putfield
            case 0xb6: // invokevirtual
            case 0xb7: // invokespecial
            case 0xb8: // invokestatic
            case 0xb9: // invokeinterface
            case 0xbb: // new
            case 0xbd: // anewarray
            case 0xc0: // checkcast
            case 0xc1: // instanceof
            case 0xc5: // multianewarray
                append(" index=\"").append(instruction.getIndex()).append("\">");
                append(instruction);
                append(" ");
                instruction.getIndexedConstantPoolEntry().accept(this);
                break;
            case 0x1a: // iload_0
            case 0x1e: // lload_0
            case 0x22: // fload_0
            case 0x26: // dload_0
            case 0x2a: // aload_0
            case 0x3b: // istore_0
            case 0x3f: // lstore_0
            case 0x43: // fstore_0
            case 0x47: // dstore_0
            case 0x4b: // astore_0
            case 0x1b: // iload_1
            case 0x1f: // lload_1
            case 0x23: // fload_1
            case 0x27: // dload_1
            case 0x2b: // aload_1
            case 0x3c: // istore_1
            case 0x40: // lstore_1
            case 0x44: // fstore_1
            case 0x48: // dstore_1
            case 0x4c: // astore_1
            case 0x1c: // iload_2
            case 0x20: // lload_2
            case 0x24: // fload_2
            case 0x28: // dload_2
            case 0x2c: // aload_2
            case 0x3d: // istore_2
            case 0x41: // lstore_2
            case 0x45: // fstore_2
            case 0x49: // dstore_2
            case 0x4d: // astore_2
            case 0x1d: // iload_3
            case 0x21: // lload_3
            case 0x25: // fload_3
            case 0x29: // dload_3
            case 0x2d: // aload_3
            case 0x3e: // istore_3
            case 0x42: // lstore_3
            case 0x46: // fstore_3
            case 0x4a: // dstore_3
            case 0x4e: // astore_3
            case 0x15: // iload
            case 0x16: // llload
            case 0x17: // fload
            case 0x18: // dload
            case 0x19: // aload
            case 0x36: // istore
            case 0x37: // lstore
            case 0x38: // fstore
            case 0x39: // dstore
            case 0x3a: // astore
            case 0xa9: // ret
                append(" index=\"").append(instruction.getIndex()).append("\">");
                append(instruction);
                appendLocalVariable(instruction.getIndexedLocalVariable());
                break;
            case 0x99: // ifeq
            case 0x9a: // ifne
            case 0x9b: // iflt
            case 0x9c: // ifge
            case 0x9d: // ifgt
            case 0x9e: // ifle
            case 0x9f: // if_icmpeq
            case 0xa0: // if_icmpne
            case 0xa1: // if_icmplt
            case 0xa2: // if_icmpge
            case 0xa3: // if_icmpgt
            case 0xa4: // if_icmple
            case 0xa5: // if_acmpeq
            case 0xa6: // if_acmpne
            case 0xa7: // goto
            case 0xa8: // jsr
            case 0xc6: // ifnull
            case 0xc7: // ifnonnull
            case 0xc8: // goto_w
            case 0xc9: // jsr_w
                append(" offset=\"").append(instruction.getOffset()).append("\">");
                append(instruction).append(" ").append(instruction.getStart() + instruction.getOffset());
                break;
            case 0x84: // iinc
                append(" index=\"").append(instruction.getIndex()).append("\" value=\"").append(instruction.getValue()).append("\">");
                append(instruction);
                appendLocalVariable(instruction.getIndexedLocalVariable());
                break;
            case 0xaa: // tableswitch
                append(" padding=\"").append(instruction.getPadding()).append("\" default=\"").appendSwitchDefault(instruction).append("\" low=\"").append(instruction.getLow()).append("\" high=\"").append(instruction.getHigh()).append("\">");
                append(instruction).append(" ").appendTableSwitch(instruction, " | ");
                break;
            case 0xab: // lookupswitch
                append(" padding=\"").append(instruction.getPadding()).append("\" default=\"").appendSwitchDefault(instruction).append("\" npairs=\"").append(instruction.getNPairs()).append("\">");
                append(instruction).append(" ").appendLookupSwitch(instruction, " | ");
                break;
            case 0xba: // invokedynamic
                append(" index=\"").append(instruction.getIndex()).append("\">");
                append(instruction);
                var indexedEntry = instruction.getIndexedConstantPoolEntry();
                if (indexedEntry instanceof Dynamic_info dynamic_info) {
                    append(" ").append(dynamic_info.getName());
                } else if (indexedEntry instanceof InvokeDynamic_info invokeDynamic_info) {
                    append(" ").append(invokeDynamic_info.getName());
                }
                // TODO: Replace with type pattern matching in switch expression in Java 21
                // switch (instruction.getIndexedConstantPoolEntry()) {
                //     case Dynamic_info entry -> append(" ").append(entry.getName());
                //     case InvokeDynamic_info entry -> append(" ").append(entry.getName());
                //     default -> append("");
                // }
                instruction.getDynamicConstantPoolEntries().forEach(entry -> {
                    append(" ");
                    entry.accept(this);
                });
                break;
            case 0xc4: // wide
                if (instruction.getByte(1) == 0x84 /* iinc */) {
                    append(" index=\"").append(instruction.getIndex()).append("\" value=\"").append(instruction.getValue()).append("\">");
                } else {
                    append(" index=\"").append(instruction.getIndex()).append("\">");
                }
                append(instruction);
                appendLocalVariable(instruction.getIndexedLocalVariable());
                break;
            default:
                append(">");
                append(instruction);
                break;
        }
        append("</instruction>").eol();
    }

    public void visitExceptionHandler(ExceptionHandler helper) {
        indent();
        append("<exception-handler>");
        append("<start-pc>").append(helper.getStartPC()).append("</start-pc>");
        append("<end-pc>").append(helper.getEndPC()).append("</end-pc>");
        append("<handler-pc>").append(helper.getHandlerPC()).append("</handler-pc>");

        append("<catch-type>");
        if (helper.hasCatchType()) {
            helper.getRawCatchType().accept(this);
        }
        append("</catch-type>");

        append("</exception-handler>").eol();
    }

    public void visitInnerClass(InnerClass helper) {
        indent().append("<inner-class access-flags=\"").append(format.format(helper.getAccessFlags())).append("\">").eol();
        raiseIndent();

        if (helper.isPublic())     indent().append("<public/>").eol();
        if (helper.isProtected())  indent().append("<protected/>").eol();
        if (helper.isPrivate())    indent().append("<private/>").eol();
        if (helper.isStatic())     indent().append("<static/>").eol();
        if (helper.isFinal())      indent().append("<final/>").eol();
        if (helper.isInterface())  indent().append("<is-interface/>").eol();
        if (helper.isAbstract())   indent().append("<abstract/>").eol();
        if (helper.isSynthetic())  indent().append("<synthetic/>").eol();
        if (helper.isAnnotation()) indent().append("<is-annotation/>").eol();
        if (helper.isEnum())       indent().append("<enum/>").eol();

        indent();
        append("<inner-class-info>");
        helper.getRawInnerClassInfo().accept(this);
        append("</inner-class-info>").eol();

        indent();
        append("<outer-class-info>");
        if (helper.hasOuterClassInfo()) {
            helper.getRawOuterClassInfo().accept(this);
        }
        append("</outer-class-info>").eol();

        indent();
        append("<inner-name>");
        if (helper.hasInnerName()) {
            helper.getRawInnerName().accept(this);
        }
        append("</inner-name>").eol();

        lowerIndent();
        indent().append("</inner-class>").eol();
    }

    public void visitLineNumber(LineNumber helper) {
        indent();
        append("<line-number>");
        append("<start-pc>").append(helper.getStartPC()).append("</start-pc>");
        append("<line>").append(helper.getLineNumber()).append("</line>");
        append("</line-number>").eol();
    }

    public void visitLocalVariable(LocalVariable helper) {
        indent();
        append("<local-variable pc=\"").append(helper.getStartPC()).append("\" length=\"").append(helper.getLength()).append("\" index=\"").append(helper.getIndex()).append("\">");
        append("<name>");
        helper.getRawName().accept(this);
        append("</name>");

        append("<type>").append(DescriptorHelper.getType(helper.getDescriptor())).append("</type>");
        append("</local-variable>").eol();
    }

    public void visitLocalVariableType(LocalVariableType helper) {
        indent();
        append("<local-variable-type pc=\"").append(helper.getStartPC()).append("\" length=\"").append(helper.getLength()).append("\" index=\"").append(helper.getIndex()).append("\">");
        append("<name>");
        helper.getRawName().accept(this);
        append("</name>");

        append("<signature>");
        helper.getRawSignature().accept(this);
        append("</signature>");
        append("</local-variable-type>").eol();
    }

    public void visitBootstrapMethod(BootstrapMethod helper) {
        indent().append("<bootstrap-method>").eol();
        raiseIndent();

        indent();
        append("<bootstrap-method-ref index=\"").append(helper.getBootstrapMethodRef()).append("\">");
        helper.getBootstrapMethod().accept(this);
        append("</bootstrap-method-ref>").eol();

        indent().append("<arguments>").eol();
        raiseIndent();

        helper.getArgumentIndices()
                .forEach(index -> {
                    indent();
                    append("<argument index=\"").append(index).append("\">");
                    helper.getArgument(index).accept(this);
                    append("</argument>").eol();
                });

        lowerIndent();
        indent().append("</arguments>").eol();

        lowerIndent();
        indent().append("</bootstrap-method>").eol();
    }

    public void visitMethodParameter(MethodParameter helper) {
        indent().append("<method-parameter access-flags=\"").append(format.format(helper.getAccessFlags())).append("\">").eol();
        raiseIndent();

        if (helper.hasName()) {
            indent();
            append("<name>");
            helper.getRawName().accept(this);
            append("</name>").eol();
        }

        if (helper.isFinal())     indent().append("<final/>").eol();
        if (helper.isSynthetic()) indent().append("<synthetic/>").eol();
        if (helper.isMandated())  indent().append("<mandated/>").eol();

        lowerIndent();
        indent().append("</method-parameter>").eol();
    }

    public void visitModuleRequires(ModuleRequires helper) {
        indent().append("<module-requires requires-flags=\"").append(format.format(helper.getRequiresFlags())).append("\">").eol();
        raiseIndent();

        indent();
        append("<module index=\"").append(helper.getRequiresIndex()).append("\">");
        helper.getRawRequires().accept(this);
        append("</module>").eol();

        if (helper.isTransitive())  indent().append("<transitive/>").eol();
        if (helper.isStaticPhase()) indent().append("<static-phase/>").eol();
        if (helper.isSynthetic())   indent().append("<synthetic/>").eol();
        if (helper.isMandated())    indent().append("<mandated/>").eol();

        if (helper.hasRequiresVersion()) {
            indent();
            append("<version index=\"").append(helper.getRequiresVersionIndex()).append("\">");
            helper.getRawRequiresVersion().accept(this);
            append("</version>").eol();
        }

        lowerIndent();
        indent().append("</module-requires>").eol();
    }

    public void visitModuleExports(ModuleExports helper) {
        indent().append("<module-exports exports-flags=\"").append(format.format(helper.getExportsFlags())).append("\">").eol();
        raiseIndent();

        indent();
        append("<package index=\"").append(helper.getExportsIndex()).append("\">");
        helper.getRawExports().accept(this);
        append("</package>").eol();

        if (helper.isSynthetic())   indent().append("<synthetic/>").eol();
        if (helper.isMandated())    indent().append("<mandated/>").eol();

        helper.getExportsTos().forEach(moduleExportsTo -> moduleExportsTo.accept(this));

        lowerIndent();
        indent().append("</module-exports>").eol();
    }

    public void visitModuleExportsTo(ModuleExportsTo helper) {
        indent().append("<module-exports-to>").eol();
        raiseIndent();

        indent();
        append("<module index=\"").append(helper.getExportsToIndex()).append("\">");
        helper.getRawExportsTo().accept(this);
        append("</module>").eol();

        lowerIndent();
        indent().append("</module-exports-to>").eol();
    }

    public void visitModuleOpens(ModuleOpens helper) {
        indent().append("<module-opens opens-flags=\"").append(format.format(helper.getOpensFlags())).append("\">").eol();
        raiseIndent();

        indent();
        append("<package index=\"").append(helper.getOpensIndex()).append("\">");
        helper.getRawOpens().accept(this);
        append("</package>").eol();

        if (helper.isSynthetic())   indent().append("<synthetic/>").eol();
        if (helper.isMandated())    indent().append("<mandated/>").eol();

        helper.getOpensTos().forEach(moduleOpensTo -> moduleOpensTo.accept(this));

        lowerIndent();
        indent().append("</module-opens>").eol();
    }

    public void visitModuleOpensTo(ModuleOpensTo helper) {
        indent().append("<module-opens-to>").eol();
        raiseIndent();

        indent();
        append("<module index=\"").append(helper.getOpensToIndex()).append("\">");
        helper.getRawOpensTo().accept(this);
        append("</module>").eol();

        lowerIndent();
        indent().append("</module-opens-to>").eol();
    }

    public void visitModuleUses(ModuleUses helper) {
        indent().append("<module-uses>").eol();
        raiseIndent();

        appendClassInfo(helper.getUsesIndex(), helper.getRawUses());

        lowerIndent();
        indent().append("</module-uses>").eol();
    }

    public void visitModuleProvides(ModuleProvides helper) {
        indent().append("<module-provides>").eol();
        raiseIndent();

        appendClassInfo(helper.getProvidesIndex(), helper.getRawProvides());

        helper.getProvidesWiths().forEach(moduleProvidesWith -> moduleProvidesWith.accept(this));

        lowerIndent();
        indent().append("</module-provides>").eol();
    }

    public void visitModuleProvidesWith(ModuleProvidesWith helper) {
        indent().append("<module-provides-with>").eol();
        raiseIndent();

        appendClassInfo(helper.getProvidesWithIndex(), helper.getRawProvidesWith());

        lowerIndent();
        indent().append("</module-provides-with>").eol();
    }

    public void visitModulePackage(ModulePackage helper) {
        indent();
        append("<package index=\"").append(helper.getPackageIndex()).append("\">");
        helper.getRawPackage().accept(this);
        append("</package>").eol();
    }

    public void visitNestMember(NestMember helper) {
        appendClassInfo(helper.getMemberClassIndex(), helper.getRawMemberClass());
    }

    public void visitRecordComponent_info(RecordComponent_info helper) {
        indent().append("<record-component>").eol();
        raiseIndent();

        indent();
        append("<name index=\"").append(helper.getNameIndex()).append("\">");
        helper.getRawName().accept(this);
        append("</name>").eol();

        indent().append("<type index=\"").append(helper.getDescriptorIndex()).append("\">").append(helper.getType()).append("</type>").eol();

        indent().append("<attributes>").eol();
        raiseIndent();

        super.visitRecordComponent_info(helper);

        lowerIndent();
        indent().append("</attributes>").eol();

        lowerIndent();
        indent().append("</record-component>").eol();
    }


    public void visitPermittedSubclass(PermittedSubclass helper) {
        appendClassInfo(helper.getSubclassIndex(), helper.getRawSubclass());
    }

    public void visitAnnotation(Annotation helper) {
        indent().append("<annotation>").eol();
        raiseIndent();

        indent().append("<type>").append(helper.getType()).append("</type>").eol();

        indent().append("<element-value-pairs>").eol();
        raiseIndent();

        super.visitAnnotation(helper);

        lowerIndent();
        indent().append("</element-value-pairs>").eol();

        lowerIndent();
        indent().append("</annotation>").eol();
    }

    public void visitParameterAnnotation(ParameterAnnotation helper) {
        indent().append("<parameter-annotation>").eol();
        raiseIndent();

        indent().append("<annotations>").eol();
        raiseIndent();

        super.visitParameterAnnotation(helper);

        lowerIndent();
        indent().append("</annotations>").eol();

        lowerIndent();
        indent().append("</parameter-annotation>").eol();
    }

    public void visitTypeAnnotation(TypeAnnotation helper) {
        indent().append("<type-annotation>").eol();
        raiseIndent();

        helper.getTarget().accept(this);

        indent().append("<target-path>").eol();
        raiseIndent();

        helper.getTargetPath().accept(this);

        lowerIndent();
        indent().append("</target-path>").eol();

        indent().append("<element-value-pairs>").eol();
        raiseIndent();

        helper.getElementValuePairs().forEach(elementValuePair -> elementValuePair.accept(this));

        lowerIndent();
        indent().append("</element-value-pairs>").eol();

        lowerIndent();
        indent().append("</type-annotation>").eol();
    }

    public void visitTypeParameterTarget(TypeParameterTarget helper) {
        indent().append("<type-parameter-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<type-parameter-index>").append(helper.getTypeParameterIndex()).append("</type-parameter-index>").eol();

        lowerIndent();
        indent().append("</type-parameter-target>").eol();
    }

    public void visitSupertypeTarget(SupertypeTarget helper) {
        indent().append("<supertype-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<supertype-index>").append(helper.getSupertypeIndex()).append("</supertype-index>").eol();

        lowerIndent();
        indent().append("</supertype-target>").eol();
    }

    public void visitTypeParameterBoundTarget(TypeParameterBoundTarget helper) {
        indent().append("<type-parameter-bound-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<type-parameter-index>").append(helper.getTypeParameterIndex()).append("</type-parameter-index>").eol();
        indent().append("<bound-index>").append(helper.getBoundIndex()).append("</bound-index>").eol();

        lowerIndent();
        indent().append("</type-parameter-bound-target>").eol();
    }

    public void visitEmptyTarget(EmptyTarget helper) {
        indent().append("<empty-target target-type=\"").append(helper.getHexTargetType()).append("\"/>").eol();
    }

    public void visitFormalParameterTarget(FormalParameterTarget helper) {
        indent().append("<formal-parameter-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<formal-parameter-index>").append(helper.getFormalParameterIndex()).append("</formal-parameter-index>").eol();

        lowerIndent();
        indent().append("</formal-parameter-target>").eol();
    }

    public void visitThrowsTarget(ThrowsTarget helper) {
        indent().append("<throws-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<throws-type-index>").append(helper.getThrowsTypeIndex()).append("</throws-type-index>").eol();

        lowerIndent();
        indent().append("</throws-target>").eol();
    }

    public void visitLocalvarTarget(LocalvarTarget helper) {
        indent().append("<localvar-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        super.visitLocalvarTarget(helper);

        lowerIndent();
        indent().append("</localvar-target>").eol();
    }

    public void visitCatchTarget(CatchTarget helper) {
        indent().append("<catch-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<exception-table-index>").append(helper.getExceptionTableIndex()).append("</exception-table-index>").eol();

        lowerIndent();
        indent().append("</catch-target>").eol();
    }

    public void visitOffsetTarget(OffsetTarget helper) {
        indent().append("<offset-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<offset>").append(helper.getOffset()).append("</offset>").eol();

        lowerIndent();
        indent().append("</offset-target>").eol();
    }

    public void visitTypeArgumentTarget(TypeArgumentTarget helper) {
        indent().append("<type-argument-target target-type=\"").append(helper.getHexTargetType()).append("\">").eol();
        raiseIndent();

        indent().append("<offset>").append(helper.getOffset()).append("</offset>").eol();
        indent().append("<type-argument-index>").append(helper.getTypeArgumentIndex()).append("</type-argument-index>").eol();

        lowerIndent();
        indent().append("</type-argument-target>").eol();
    }

    public void visitTypePathEntry(TypePathEntry helper) {
        indent().append("<type-path>").eol();
        raiseIndent();

        indent().append("<type-path-kind>").append(helper.getTypePathKind().getTypePathKind()).append("</type-path-kind>").eol();
        indent().append("<type-argument-index>").append(helper.getTypeArgumentIndex()).append("</type-argument-index>").eol();

        lowerIndent();
        indent().append("</type-path>").eol();
    }

    public void visitElementValuePair(ElementValuePair helper) {
        indent().append("<element-value-pair>").eol();
        raiseIndent();

        indent().append("<element-name>").append(helper.getElementName()).append("</element-name>").eol();

        super.visitElementValuePair(helper);

        lowerIndent();
        indent().append("</element-value-pair>").eol();
    }

    public void visitByteConstantElementValue(ByteConstantElementValue helper) {
        visitConstantElementValue(helper, "byte");
    }

    public void visitCharConstantElementValue(CharConstantElementValue helper) {
        visitConstantElementValue(helper, "char");
    }

    public void visitDoubleConstantElementValue(DoubleConstantElementValue helper) {
        visitConstantElementValue(helper, "double");
    }

    public void visitFloatConstantElementValue(FloatConstantElementValue helper) {
        visitConstantElementValue(helper, "float");
    }

    public void visitIntegerConstantElementValue(IntegerConstantElementValue helper) {
        visitConstantElementValue(helper, "integer");
    }

    public void visitLongConstantElementValue(LongConstantElementValue helper) {
        visitConstantElementValue(helper, "long");
    }

    public void visitShortConstantElementValue(ShortConstantElementValue helper) {
        visitConstantElementValue(helper, "short");
    }

    public void visitBooleanConstantElementValue(BooleanConstantElementValue helper) {
        visitConstantElementValue(helper, "boolean");
    }

    public void visitStringConstantElementValue(StringConstantElementValue helper) {
        visitConstantElementValue(helper, "string");
    }

    private void visitConstantElementValue(ConstantElementValue helper, String type) {
        indent();
        append("<").append(type).append("-element-value tag=\"").append(helper.getTag()).append("\">");
        helper.getRawConstValue().accept(this);
        append("</").append(type).append("-element-value>").eol();
    }

    public void visitEnumElementValue(EnumElementValue helper) {
        indent();
        append("<enum-element-value tag=\"").append(helper.getTag()).append("\">");
        append(helper.getTypeName()).append(".").append(helper.getConstName());
        append("</enum-element-value>").eol();
    }

    public void visitClassElementValue(ClassElementValue helper) {
        indent();
        append("<class-element-value tag=\"").append(helper.getTag()).append("\">");
        append(helper.getClassInfo());
        append("</class-element-value>").eol();
    }

    public void visitAnnotationElementValue(AnnotationElementValue helper) {
        indent().append("<annotation-element-value tag=\"").append(helper.getTag()).append("\">").eol();
        raiseIndent();

        super.visitAnnotationElementValue(helper);

        lowerIndent();
        indent().append("</annotation-element-value>").eol();
    }

    public void visitArrayElementValue(ArrayElementValue helper) {
        indent().append("<array-element-value tag=\"").append(helper.getTag()).append("\">").eol();
        raiseIndent();

        super.visitArrayElementValue(helper);

        lowerIndent();
        indent().append("</array-element-value>").eol();
    }

    public void visitLocalvarTableEntry(LocalvarTableEntry helper) {
        indent().append("<localvar start-pc=\"").append(helper.getStartPc()).append("\" length=\"").append(helper.getLength()).append("\" index=\"").append(helper.getIndex()).append("\"/>").eol();
    }

    public void visitSameFrame(SameFrame helper) {
        indent().append("<same-frame frame-type=\"").append(helper.getFrameType()).append("\"/>").eol();
    }

    public void visitSameLocals1StackItemFrame(SameLocals1StackItemFrame helper) {
        indent().append("<same-locals-1-stack-item-frame frame-type=\"").append(helper.getFrameType()).append("\">").eol();
        raiseIndent();

        indent().append("<stack>").eol();
        raiseIndent();
        super.visitSameLocals1StackItemFrame(helper);
        lowerIndent();
        indent().append("</stack>").eol();

        lowerIndent();
        indent().append("</same-locals-1-stack-item-frame>").eol();
    }

    public void visitSameLocals1StackItemFrameExtended(SameLocals1StackItemFrameExtended helper) {
        indent().append("<same-locals-1-stack-item-frame-extended frame-type=\"").append(helper.getFrameType()).append("\" offset-delta=\"").append(helper.getOffsetDelta()).append("\">").eol();
        raiseIndent();

        indent().append("<stack>").eol();
        raiseIndent();
        super.visitSameLocals1StackItemFrameExtended(helper);
        lowerIndent();
        indent().append("</stack>").eol();

        lowerIndent();
        indent().append("</same-locals-1-stack-item-frame-extended>").eol();
    }

    public void visitChopFrame(ChopFrame helper) {
        indent().append("<chop-frame frame-type=\"").append(helper.getFrameType()).append("\" offset-delta=\"").append(helper.getOffsetDelta()).append("\"/>").eol();
    }

    public void visitSameFrameExtended(SameFrameExtended helper) {
        indent().append("<same-frame-extended frame-type=\"").append(helper.getFrameType()).append("\" offset-delta=\"").append(helper.getOffsetDelta()).append("\"/>").eol();
    }

    public void visitAppendFrame(AppendFrame helper) {
        indent().append("<append-frame frame-type=\"").append(helper.getFrameType()).append("\" offset-delta=\"").append(helper.getOffsetDelta()).append("\">").eol();
        raiseIndent();

        indent().append("<locals>").eol();
        raiseIndent();
        super.visitAppendFrame(helper);
        lowerIndent();
        indent().append("</locals>").eol();

        lowerIndent();
        indent().append("</append-frame>").eol();
    }

    public void visitFullFrame(FullFrame helper) {
        indent().append("<full-frame frame-type=\"").append(helper.getFrameType()).append("\" offset-delta=\"").append(helper.getOffsetDelta()).append("\">").eol();
        raiseIndent();

        indent().append("<locals>").eol();
        raiseIndent();
        helper.getLocals().forEach(local -> local.accept(this));
        lowerIndent();
        indent().append("</locals>").eol();

        indent().append("<stack>").eol();
        raiseIndent();
        helper.getStack().forEach(local -> local.accept(this));
        lowerIndent();
        indent().append("</stack>").eol();

        lowerIndent();
        indent().append("</full-frame>").eol();
    }

    public void visitTopVariableInfo(TopVariableInfo helper) {
        indent().append("<top-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitIntegerVariableInfo(IntegerVariableInfo helper) {
        indent().append("<integer-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitFloatVariableInfo(FloatVariableInfo helper) {
        indent().append("<float-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitLongVariableInfo(LongVariableInfo helper) {
        indent().append("<long-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitDoubleVariableInfo(DoubleVariableInfo helper) {
        indent().append("<double-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitNullVariableInfo(NullVariableInfo helper) {
        indent().append("<null-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitUninitializedThisVariableInfo(UninitializedThisVariableInfo helper) {
        indent().append("<uninitialized-this-variable-info tag=\"").append(helper.getTag()).append("\"/>").eol();
    }

    public void visitObjectVariableInfo(ObjectVariableInfo helper) {
        indent().append("<object-variable-info tag=\"").append(helper.getTag()).append("\">").eol();
        raiseIndent();

        top = true;
        super.visitObjectVariableInfo(helper);
        top = false;

        lowerIndent();
        indent().append("</object-variable-info>").eol();
    }

    public void visitUninitializedVariableInfo(UninitializedVariableInfo helper) {
        indent().append("<uninitialized-variable-info tag=\"").append(helper.getTag()).append("\" offset=\"").append(helper.getOffset()).append("\"/>").eol();
    }

    private Printer appendLocalVariable(LocalVariable localVariable) {
        if (localVariable != null) {
            append(" ");
            append(DescriptorHelper.getType(localVariable.getDescriptor())).append(" ").append(localVariable.getName());
        }
        return this;
    }

    private Printer appendClassInfo(int index, Class_info class_info) {
        indent();
        append("<class index=\"").append(index).append("\">");
        class_info.accept(this);
        append("</class>").eol();
        return this;
    }

    // Visible for testing
    String escapeXMLCharacters(String text) {
        StringBuilder result = new StringBuilder();

        text.codePoints()
                .forEach(c -> {
                    if (c == '&') {
                        result.append("&amp;");
                    } else if (c == '<') {
                        result.append("&lt;");
                    } else if (c == '>') {
                        result.append("&gt;");
                    } else if (Character.isISOControl(c) || c > 0x9F) {
                        result.append("&#x");
                        result.append(Integer.toString(c, 16).toUpperCase());
                        result.append(";");
                    } else {
                        result.append(Character.toChars(c));
                    }
                });

        if (containsControlCharacters(text)) {
            return "<![CDATA[" + result + "]]>";
        } else {
            return result.toString();
        }
    }

    private boolean containsControlCharacters(String text) {
        return text.codePoints().anyMatch(c -> Character.isISOControl(c) || c > 0x9F);
    }
}