JavaHT TreeMaker

Implementing run() method

The run() method is place where we you API for modifications. We decided to add java.io.Externalizable interface the class declaration.

Original source looks like:

    package org.netbeans.test.codegen;

    public class Tutorial1 {
    }

At the end, we want to see source like:

    package org.netbeans.test.codegen;

    import java.io.Externalizable;

    public class Tutorial1 implements Externalizable {
    }

In short, this can be done with this code:

    public void run(WorkingCopy workingCopy) throws IOException {

        workingCopy.toPhase(Phase.RESOLVED); // is it neccessary?

        CompilationUnitTree cut = workingCopy.getCompilationUnit();
        TreeMaker make = workingCopy.getTreeMaker();

        for (Tree typeDecl : cut.getTypeDecls()) {
            if (Tree.Kind.CLASS == typeDecl.getKind()) {
                ClassTree clazz = (ClassTree) typeDecl;
                ExpressionTree implementsClause = make.Identifier("Externalizable");

                implementsClause = make.Identifier("java.io.Externalizable");
                        
                TypeElement element = workingCopy.getElements().getTypeElement("java.io.Externalizable");
                implementsClause = make.QualIdent(element);
                        
                ClassTree modifiedClazz = make.addClassImplementsClause(clazz, implementsClause);
                workingCopy.rewrite(clazz, modifiedClazz);
            } // end if
        } // end for
    } // end run

Here is steps description:

  • workingCopy.toPhase(Phase.RESOLVED); -- Resolves symbols for provided java source.
  • CompilationUnitTree cut = workingCopy.getCompilationUnit(); -- Instance represents one java source file, exactly as defined in JLS, §7.3 Compilation Units.
  • TreeMaker make = workingCopy.getTreeMaker(); -- Get the tree maker, the core class used for making modifications. It allows to add new members to class, modify statements, etc.
  • for (Tree typeDecl : cut.getTypeDecls()) { ... } -- Go through all top level declarations (JLS §7.6).
  • if (Tree.Kind.CLASS == typeDecl.getKind()) { ...} -- Ensure about type - not neccessary here, when we omit that Annotation Type can be also declared here. This is important and you will see it perhaps on other places too -- always, you have to use Kind for checking instance! instanceof operator shouldn't be used for such a test.
  • ClassTree clazz = (ClassTree) typeDecl;
  • Create identifier:
  • ExpressionTree implementsClause = make.Identifier("Externalizable"); -- Simpliest, but not sufficient solution: Add the plain identifier. It generates source as you can see below, but when import is not available, identifier is not resolved and class will not compile.
    public class Tutorial1 implements Externalizable {
    }
  • ExpressionTree implementsClause = make.Identifier("java.io.Externalizable"); -- We can solve described problem with specifying fully-qualified name. We can create again identifier tree. (Bear in mind, that you will never get such an identifier - meant dot separated - from the compiler staff. Note: Should we consider it as incorrect usage?) The result will be compilable, see code below. The disadvantage is fully-qualified name in declaration.
    public class Tutorial1 implements java.io.Externalizable {
    }
  • Last, and perhaps the most often used solution is to add plain identifier to type declaration and correct import statement to compilation unit. It can be done by following statements: TypeElement element = workingCopy.getElements().getTypeElement("java.io.Externalizable"); -- You will get resolved element. You should check, that element is available. Then, make QualIdent tree: implementsClause = make.QualIdent(element); -- The QualIdent will be recognized during source code modification and engine will decide (in accordance with options), how to correctly generate. When using default settings, import for your class will be added and simple name will be used in implements clause:
    import java.io.Externalizable;

    public class Tutorial1 implements Externalizable {
    }
  • ClassTree modifiedClazz = make.addClassImplementsClause(clazz, implementsClause); -- Use tree maker method to add the interface identifier to the 'implements' clause. Bear in mind that this operation just put it to the tree, not to the source file. Because nodes in tree are immutable, method returns the same class type as provided in first parameter, in our case ClassTree. In other words, if a method takes ClassTree parameter, it will return another class tree, which contains provided modification.
  • workingCopy.rewrite(clazz, modifiedClazz); -- Replace the original node with the new one. It adds the change to the list of changes, later used for making source modification.

Adding method with body

Next example show more complex task. Adding method to type declaration. The steps described above are the same, we just implement run() method of CancellableTask. Here is the code we want to add to class declaration:

    public void writeExternal(final ObjectOutput arg0) throws IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

We have to prepare all elements belonging to method. First of all, prepare modifiers for the method. We use TreeMaker instance make again.

    public void run(WorkingCopy workingCopy) throws IOException {
        ...
        // create method modifier: public and no annotation
        ModifiersTree methodModifiers = make.Modifiers(
            Collections.<Modifier>singleton(Modifier.PUBLIC),
            Collections.<AnnotationTree>emptyList()
        );
        ...
    }

Next step is preparing method parameter arg0 of type ObjectOutput and modifier final:

                     
    public void run(WorkingCopy workingCopy) throws IOException {
        ...
        // create parameter:
        // final ObjectOutput arg0
        VariableTree parameter = make.Variable(
            make.Modifiers(
                Collections.<Modifier>singleton(Modifier.FINAL),
                Collections.<AnnotationTree>emptyList()
            ),
            "arg0", // name
            make.Identifier("Object"), // parameter type
            null // initializer - does not make sense in parameters.
        );
        ...
    }

Method throws exception, prepare exception identifier IOException. It is the same when we prepared interface for implements clause.

    public void run(WorkingCopy workingCopy) throws IOException {
        ...
        // prepare simple name to throws clause:
        // 'throws IOException' and its import will be added (if it is not available yet)
        TypeElement element = workingCopy.getElements().getTypeElement("java.io.IOException");
        ExpressionTree throwsClause = make.QualIdent(element);
        ...
    }

We have everything, what we need for method creation. Make method:

    public void run(WorkingCopy workingCopy) throws IOException {
        ...
        // create method.
        MethodTree newMethod = make.Method(
            methodModifiers, // public
            "writeExternal", // writeExternal
            make.PrimitiveType(TypeKind.VOID), // return type "void"
            Collections.<TypeParameterTree>emptyList(), // type parameters - none
            Collections.<VariableTree>singletonList(parameter), // final ObjectOutput arg0
            Collections.<ExpressionTree>singletonList(throwsClause), // throws 
            "{ throw new UnsupportedOperationException(\"Not supported yet.\") }", // body text
            null // default value - not applicable here, used by annotations
        );
        ...
    }

In the example above, we used the most often used factory method for source code method creation. It contains string for its body. You can add it as plain syntax correct text and engine will do imports and formatting stuff for you. There is also second method, which allows to add the body as a block:

    public void run(WorkingCopy workingCopy) throws IOException {
        ...
        // create method.
        MethodTree newMethod = make.Method(
            methodModifiers, // public
            "writeExternal", // writeExternal
            make.PrimitiveType(TypeKind.VOID), // return type "void"
            Collections.<TypeParameterTree>emptyList(), // type parameters - none
            Collections.<VariableTree>singletonList(parameter), // final ObjectOutput arg0
            Collections.<ExpressionTree>singletonList(throwsClause), // throws 
            make.Block(Collections.<StatementTree>emptyList(), false), // empty statement block
            null // default value - not applicable here, used by annotations
        );
        ...
    }

Example creates method with empty body.

At the end, do not forget to add it to type declaration and register change on working copy:


        // and in the same way as interface was added to implements clause,
        // add feature to the class as its member:
        ClassTree modifiedClazz = make.addClassMember(clazz, newMethod);
        workingCopy.rewrite(clazz, modifiedClazz);
        ...
    }

Do you want to see it in a practice? Open the java/source project, go to unit test packages, then org.netbeans.api.java.source.gen package, open file TutorialTest.java and run it in IDE. You can experiment with it little bit.

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo