Java Online Compiler

Beknazar
5 min readFeb 13, 2021

--

Well, it’s not really an online compiler. It is a system that allows practicing Java coding from the Web.

I’m working as a Java instructor at Tech Lead Academy. Right from the beginning, I realize, I need an automated system to validate students' code and which will help them practice. CodingBat was a perfect solution to start. While CodingBat is the best platform, it had few disadvantages in our case. Firstly, I wasn’t able to see how does class is doing on assignments I give from CodingBat. Secondly, in the CodingBat we don’t have a very beginning stage where students just print something in the main method or declare variables, and there is no practice in OOP and overall working with classes and objects. This led me to start thinking to create my own system.

In this article, I want to discuss my challenges and share my experience building this system.

To start, we will focus on the compilation part, and then we will go to the validation part.

The server should have JDK installed

The idea is simple, we have a program — compiler. It takes source code and converts it into java bytecode then java bytecode can be executed by JVM.

High-level steps:

  1. Get student code from a website as text(string) to server-side where we have JDK installed(JDK includes a compiler and JVM).
  2. Compile student code. We will need to create a java file with the correct name and I used JavaCompiler which can invoke Java™ programming language compilers from a program.
  3. Execute our compiled file with java <className> command and catch the output of the program and send it to the client-side. I used ProcessBuilder to run commands from the terminal.

Let’s take a look at the code.

This interface for compiling and running source code:

package com.tla.doit.service;

import java.io.File;

/**
*
@author Beknazar Suranchiyev
* This is JDK interface to able compile and run java source code
*/
public interface JDKService {
boolean compile(File sourceFileToCompile);
String run(File javaByteCodeFileToRun);
String execute(File sourceFileToCompileAndRun);
}

This is an implementation for the JDKService interface. I have commented out the log4j part so you can just insert this code in any project and it will work.

package com.tla.doit.service.impl;

import com.tla.doit.service.JDKService;
// import org.apache.log4j.LogManager;
// import org.apache.log4j.Logger;

import javax.tools.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
*
@author Beknazar Suranchiyev
* This is JDK interface implementation to able compile and run java source code
*/
public class JDK implements JDKService {
// private static final Logger logger = LogManager.getLogger(JDK.class.getName());

private static boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
private List<String> compilerErrorMsg = new ArrayList<>();
private List<String> output = new ArrayList<>();

public List<String> getCompilerErrorMsg() {
return compilerErrorMsg;
}

public List<String> getOutput() {
return output;
}

@Override
public boolean compile(File sourceFileToCompile) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

if(compiler.getSourceVersions().size() == 0) {
//logger.error("There is no JDK in this machine.");
throw new RuntimeException("There is no JDK in this machine.");
}

DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
StandardJavaFileManager manager = compiler.getStandardFileManager(
diagnostics, null, null );

Iterable< ? extends JavaFileObject> sources =
manager.getJavaFileObjectsFromFiles( Arrays.asList( sourceFileToCompile ) );

JavaCompiler.CompilationTask task = compiler.getTask( null, manager, diagnostics,
null, null, sources );

boolean isCompiled = task.call();

if(!isCompiled) {
//logger.debug("source code failed to compile.");
compilerErrorMsg = diagnostics.getDiagnostics().stream().map(element -> element.toString()).collect(Collectors.toList());
}

return isCompiled;
}

@Override
public String run(File javaByteCodeFileToRun) {
if (javaByteCodeFileToRun == null || !javaByteCodeFileToRun.exists()) {
throw new RuntimeException("File does not exist: " + javaByteCodeFileToRun);
} else if (!javaByteCodeFileToRun.getName().endsWith(".class")) {
throw new RuntimeException("Not supported format, provide java byte code withing .class file");
}

String command = "java " + javaByteCodeFileToRun.getName().substring(0, javaByteCodeFileToRun.getName().indexOf(".class"));
//logger.debug("Run command: " + command);
try {
runCommand(javaByteCodeFileToRun.getParentFile(), command);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error while running: " + e);
}

StringBuffer outputBfr = new StringBuffer();
for (String line : output) {
outputBfr.append(line + "\n");
}
return outputBfr.toString();
}

@Override
public String execute(File sourceFileToCompileAndRun) {
if (compile(sourceFileToCompileAndRun)) {
File javaByteCodeToRun = new File(
sourceFileToCompileAndRun.toString()
.substring(0,sourceFileToCompileAndRun.toString().indexOf(".java")) + ".class");
return run(javaByteCodeToRun);
} else {
throw new RuntimeException("Failed to compiled. Catch this exception and use getCompilerErrorMsg() to get error message");
}
}

public void runCommand(File whereToRun, String command) throws Exception {
// logger.debug("Running in: " + whereToRun);
// logger.debug("Command: " + command);

ProcessBuilder builder = new ProcessBuilder();
builder.directory(whereToRun);

if(isWindows) {
builder.command("cmd.exe", "/c", command);
}else {
builder.command("sh", "-c", command);
}

Process process = builder.start();

OutputStream outputStream = process.getOutputStream();
InputStream inputStream = process.getInputStream();
InputStream errorStream = process.getErrorStream();

setStream(inputStream);
setStream(errorStream);

boolean isFinished = process.waitFor(30, TimeUnit.SECONDS);
outputStream.flush();
outputStream.close();

if(!isFinished) {
process.destroyForcibly();
}
}

private void setStream(InputStream inputStream) {
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while((line = bufferedReader.readLine()) != null) {
output.add(line);
// logger.debug(line);
}

}catch (IOException e) {
e.printStackTrace();
}
}
}

And test class. execute(File sourceCodeFile) method will compile, run, and return output. If there is a compiler error, it will throw a runtime exception. You can catch this exception and getCompilerErrorMsg() from JDK object. There is also an option to compile and run separately.

package com.tla.doit.service.impl;

import java.io.File;

public class JDKTest {
public static void main(String[] args) {
JDK obj = new JDK();
File sourceFile = new File("C:\\Users\\Beknazar\\Desktop\\Main.java");
System.out.println(obj.execute(sourceFile)); // Hello, World!
}
}

Main.java under my Desktop

public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

In the evaluation part, each assignment is a separate java maven project. It has the evaluation code with JUnit test cases and built as an executable fat jar file. Once we run it as a jar file I made it generate a report.json file that has actual results from test execution. We can create functionality to create this assignment jar projects from UI with instructor role. Challange was here to catch JUnit results, we have to implement a custom listener that implements TestExecutionListener.

In the server controller, I will check if student code if it’s compiling and if it’s I will go ahead and pull a specific jar file that already has compiled test cases. Next, I will replace the main assignment .class file with the students’ compiled file and run the whole jar. It will generate our report file, I will read it and send it back to the client-side. The main challenge here was actually replacing project code(already compiled java byte code) with student’s code(already compiled java byte code) in the jar file. This snippet can be used to do so

private static boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");public void replaceSourceFile(File projectLocation) throws Exception {
ProcessBuilder builder = new ProcessBuilder();
builder.directory(projectLocation);

if(isWindows) {
builder.command("cmd.exe", "/c", "jar uf porject.jar Project.class");
}else {
builder.command("sh", "-c", "jar uf project.jar Project.class");
}

Process process = builder.start();
int status = process.waitFor();

if(status != 0) {
logger.error("Error while replacing a source file: jar uf project.jar Project.class");
throw new RestException(500, "Error while replacing a source file: jar uf project.jar Project.class");
}
}

I think this is all you will need to build your own system. I will skip the parts which are for other functionalities like managing students and so on.

My next steps of enhancement of the system:

  1. Scalability, I want to containerize the compilation and execution part and manage with some load balancer.
  2. More metrics about student activity in the system. Like time spent on the assignment.
  3. Achievement badges.

Hope it helped you. If you have any further questions put a comment and I will do my best to help. Thank you!

--

--

Beknazar
Beknazar

No responses yet