Editing/Modifying a .java file programmatically? (not the .class file)

I know it's been a while since the original post, but one of the more accessible looking Java transformation libraries appears to be Spoon.

From the Spoon Homepage:

Spoon enables you to transform (see below) and analyze (see example) source code. Spoon provides a complete and fine-grained Java metamodel where any program element (classes, methods, fields, statements, expressions...) can be accessed both for reading and modification. Spoon takes as input source code and produces transformed source code ready to be compiled.

Update: Square have also created the JavaPoet source-code generation library, the fluent API looks simple enough to grasp.


...I would like the API to modify the java file directly/ create a modified copy of it. Is there a way to doing this?

JavaParser is an API that allows you to read in Java files, modify them, and get the results as a String.

More specifically, JavaParser parses the file and builds an AST (abstract syntax tree). You can then modify the JavaParser AST representing your source code using the API and retrieve the String representation of the AST.

I do have a .java file already and would like to add a few lines of code to a method inside it.

Here's an example of using JavaParser to add a line onto the end of a method body and print the result:

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Optional;

public class Main {
  public static void someMethod() {
    // Lines will be added here.
  }

  public static void main( String[] args ) throws FileNotFoundException {
    String newStatement = "System.out.println(\"Hello world!\");";
    File myClassSourceFile = new File( "Main.java" );

    JavaParser parser = new JavaParser();

    ParseResult<CompilationUnit> pr = parser.parse( myClassSourceFile );
    Optional<CompilationUnit> ocu = pr.getResult();

    if( ocu.isPresent() ) {
      CompilationUnit cu = ocu.get();
      ClassOrInterfaceDeclaration decl = cu.getClassByName( "Main" ).get();
      MethodDeclaration method = decl.getMethods().get( 0 );
      method.getBody().ifPresent( ( b ) -> b.addStatement( newStatement ) );
    }

    // Print out the resulting Java source code.
    System.out.println( pr.toString() );
  }
}

CompilationUnit - From JavaParser's javadoc, "This class represents the entire compilation unit. Each java file denotes a compilation unit."

In your code, replace Option.get() calls with proper handling.


An example to add method logging to a class name given on the command line:

public class Main {
  public static void main( final String[] args ) throws FileNotFoundException {
    final File sourceFile = new File( args[ 0 ] );
    final JavaParser parser = new JavaParser();
    final ParseResult<CompilationUnit> pr = parser.parse( sourceFile );
    final Optional<CompilationUnit> ocu = pr.getResult();

    if( ocu.isPresent() ) {
      final CompilationUnit cu = ocu.get();
      final List<TypeDeclaration<?>> types = cu.getTypes();

      for( final TypeDeclaration<?> type : types ) {
        final List<MethodDeclaration> methods = type.getMethods();

        for( final MethodDeclaration method : methods ) {
          final Optional<BlockStmt> body = method.getBody();
          final String m = format( "%s::%s( %s )",
                                   type.getNameAsString(),
                                   method.getNameAsString(),
                                   method.getParameters().toString() );

          final String mBegan = format(
              "System.out.println(\"BEGAN %s\");", m );
          final String mEnded = format(
              "System.out.println(\"ENDED %s\");", m );

          final Statement sBegan = parseStatement( mBegan );
          final Statement sEnded = parseStatement( mEnded );

          body.ifPresent( ( b ) -> {
            final int i = b.getStatements().size();

            b.addStatement( 0, sBegan );

            // Insert before any "return" statement.
            b.addStatement( i, sEnded );
          } );
        }

        System.out.println( cu.toString() );
      }
    }
  }
}

This will write the changed source file to standard output. If you put the Main file inside the core project's main package, then you can build the core project's JAR file (e.g., mvn package). Renaming the JAR file to javaparser.jar and then run the Main over all the JAR files:

for i in $(find . -type f -name "*.java"); do \
  java -cp javaparser.jar com.github.javaparser.Main "$i" > \
    "$i.jp";
done

Of course, it would be much more efficient to have Java iterate over a directory tree. Once the .jp files are present and look okay, you can rename them en masse using:

find . -type f -name "*jp" -size +100c -exec \
  sh -c 'mv {} $(dirname {})/$(basename {} .jp)' \;

This will destroy the original formatting, making it fairly unsuitable for checking into a repository. Some Java 14 statements might not translate into a file that can be compiled. YMMV.