Monthly Archives: February 2014

Java: Programmatically Compile and Unit Test Generated Groovy Source Code

My previous post shows how you can programmatically compile and unit test the generated Java source code. In this example, we will programmatically compile and unit test the generated Groovy source code.

Let’s assume we have the following service class that generates Groovy source code as one big String:-

public class GroovyCodeGeneratorService {
    public String generate(String packageName, String className) {
        return "package " + packageName +
               "\nclass " + className + " {" +
               "\n    boolean isOne(Integer i) {" +
               "\n      return i == 1" +
               "\n    }" +
               "\n}";
    }
}

To unit test the generated Groovy code, it is much simpler than Java:-

package com.choonchernlim.epicapp;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;

public class GroovyCodeGeneratorServiceTest {

    GroovyCodeGeneratorService groovyCodeGeneratorService = new GroovyCodeGeneratorService();

    @Test
    public void testGeneratedCode() throws Exception {
        String packageName = "com.choonchernlim.epicapp.service.impl";
        String className = "HelloWorld";

        String groovySource = groovyCodeGeneratorService.generate(packageName, className);

        ClassLoader parent = getClass().getClassLoader();
        GroovyClassLoader loader = new GroovyClassLoader(parent);

        Class groovyClass = loader.parseClass(groovySource);

        assertThat(invokeMethod(groovyClass, 1), is(true));
        assertThat(invokeMethod(groovyClass, 2), is(false));
        assertThat(invokeMethod(groovyClass, 0), is(false));
        assertThat(invokeMethod(groovyClass, -1), is(false));
    }

    private boolean invokeMethod(Class groovyClass, Integer input) throws Exception {
        GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
        return (Boolean) groovyObject.invokeMethod("isOne", input);
    }
}

Java: Programmatically Compile and Unit Test Generated Java Source Code

In one of the projects I’m currently working on, I have to write a parser to translate a scripting language into Java code. This tutorial shows how you can unit test the generated Java code.

Let’s assume we have JavaCodeGeneratorService that looks something like this:-

public class JavaCodeGeneratorService {
    public String generate(String packageName, String className) {
        return "package " + packageName + ";" +
               "public class " + className + " {" +
               "    public boolean isOne(Integer i) {" +
               "      return i == 1;" +
               "    }" +
               "}";
    }
}

This service class generates Java source code as one big String. To unit test this code, we have to:-

  1. Programmatically compile the generated source code.
  2. Instantiate the class and invoke isOne(...) to get the returned value.
    1. So, the strategy here is to create an in-memory Java file object so that we don’t have to perform any clean ups. Then, we use reflection to invoke that API to get the returned value.

      package com.choonchernlim.epicapp;
      
      import com.choonchernlim.epicapp.service.JavaCodeGeneratorService;
      import org.apache.log4j.Logger;
      import static org.hamcrest.core.Is.is;
      import static org.junit.Assert.assertThat;
      import static org.junit.Assert.fail;
      import org.junit.Test;
      
      import javax.tools.Diagnostic;
      import javax.tools.DiagnosticCollector;
      import javax.tools.JavaCompiler;
      import javax.tools.JavaFileObject;
      import javax.tools.SimpleJavaFileObject;
      import javax.tools.StandardJavaFileManager;
      import javax.tools.StandardLocation;
      import javax.tools.ToolProvider;
      import java.io.File;
      import java.io.IOException;
      import java.net.URI;
      import java.util.Arrays;
      import java.util.Locale;
      
      public class JavaCodeGeneratorServiceTest {
      
          // in-memory Java file object
          class InMemoryJavaFileObject extends SimpleJavaFileObject {
              private String contents = null;
      
              public InMemoryJavaFileObject(String className, String contents)
                      throws Exception {
                  super(URI.create("string:///" + className.replace('.', '/') +
                                   Kind.SOURCE.extension), Kind.SOURCE);
                  this.contents = contents;
              }
      
              public CharSequence getCharContent(boolean ignoreEncodingErrors)
                      throws IOException {
                  return contents;
              }
          }
      
          private static Logger log = Logger.getLogger(SomeTest.class);
      
          JavaCodeGeneratorService javaCodeGeneratorService = new JavaCodeGeneratorService();
      
          @Test
          public void testGeneratedCode() throws Exception {
              String packageName = "com.choonchernlim.epicapp.service.impl";
              String className = "HelloWorld";
      
              compile(className, javaCodeGeneratorService.generate(packageName, className));
      
              assertThat(invokeMethod(packageName, className, 1), is(true));
              assertThat(invokeMethod(packageName, className, 2), is(false));
              assertThat(invokeMethod(packageName, className, 0), is(false));
              assertThat(invokeMethod(packageName, className, -1), is(false));
          }
      
          // creates an in-memory Java file object and compile it
          private void compile(String className, String code) throws Exception {
              // Create an in-memory Java file object
              JavaFileObject javaFileObject = new InMemoryJavaFileObject(className, code);
      
              JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      
              StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,
                                                                                    null,
                                                                                    null);
      
              // If the location of the new class files is not specified, it will be
              // placed at the project's root directory. Since this is a Maven project,
              // we will comply to the Maven structure and place the generated classes
              // under `target/classes` directory. Otherwise, we may get
              // `ClassNotFoundException` when we do the reflection.
              Iterable<File> files = Arrays.asList(new File("target/classes"));
              fileManager.setLocation(StandardLocation.CLASS_OUTPUT, files);
      
              // When shit happens, the `DiagnosticCollector` may reveal useful error messages
              DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
      
              JavaCompiler.CompilationTask task = compiler.getTask(null,
                                                                   fileManager,
                                                                   diagnostics,
                                                                   null,
                                                                   null,
                                                                   Arrays.asList(javaFileObject));
      
              boolean success = task.call();
      
              fileManager.close();
      
              // If there' a compilation error, display error messages and fail the test
              if (!success) {
                  for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                      log.fatal("Code: " + diagnostic.getCode());
                      log.fatal("Kind: " + diagnostic.getKind());
                      log.fatal("Position: " + diagnostic.getPosition());
                      log.fatal("Start Position: " + diagnostic.getStartPosition());
                      log.fatal("End Position: " + diagnostic.getEndPosition());
                      log.fatal("Source: " + diagnostic.getSource());
                      log.fatal("Message: " + diagnostic.getMessage(Locale.getDefault()));
                  }
      
                  fail("Compilation failed!");
              }
          }
      
          // executes in-memory Java file object with input value
          private boolean invokeMethod(String classPackage, String className, Integer input) 
                  throws Exception {
              Class<?> clazz = Class.forName(classPackage + "." + className);
              return (Boolean) clazz.getDeclaredMethod("isOne", Integer.class)
                                    .invoke(clazz.newInstance(), input);
          }
      }
      

Java: Properly Indenting XML String

PROBLEM

Let’s assume we want to perform a pretty print on the following XML string:-

<root>
   
<name>Coco Puff</name>
        <total>10</total>    </root>

We have the following code to create a formatted XML string:-

String xml = "<root>" +
             "\n   " +
             "\n<name>Coco Puff</name>" +
             "\n        <total>10</total>    </root>";

try {
    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

    StringWriter stringWriter = new StringWriter();
    StreamResult streamResult = new StreamResult(stringWriter);

    transformer.transform(new StringSource(xml), streamResult);

    System.out.println(stringWriter.toString());
}
catch (Exception e) {
    e.printStackTrace();
}

However, the generated output looks like just the original XML string:-

<root>
   
<name>Coco Puff</name>
        <total>10</total>    </root>

Why?

SOLUTION

You may think the above code is wrong, but it is not. Based on the DOM specification, whitespaces outside the tags are perfectly valid and they are properly preserved. To remove them, we can use XPath’s normalize-space to locate all the whitespace nodes and remove them first.

String xml = "<root>" +
             "\n   " +
             "\n<name>Coco Puff</name>" +
             "\n        <total>10</total>    </root>";

try {
    Document document = DocumentBuilderFactory.newInstance()
            .newDocumentBuilder()
            .parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8"))));

    XPath xPath = XPathFactory.newInstance().newXPath();
    NodeList nodeList = (NodeList) xPath.evaluate("//text()[normalize-space()='']",
                                                  document,
                                                  XPathConstants.NODESET);

    for (int i = 0; i < nodeList.getLength(); ++i) {
        Node node = nodeList.item(i);
        node.getParentNode().removeChild(node);
    }

    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

    StringWriter stringWriter = new StringWriter();
    StreamResult streamResult = new StreamResult(stringWriter);

    transformer.transform(new DOMSource(document), streamResult);

    System.out.println(stringWriter.toString());
}
catch (Exception e) {
    e.printStackTrace();
}

When we run the above code, we will get the intended output:-

<root>
    <name>Coco Puff</name>
    <total>10</total>
</root>