diff --git a/.github/workflows/feature-branches.yml b/.github/workflows/feature-branches.yml new file mode 100644 index 0000000..68cd5a3 --- /dev/null +++ b/.github/workflows/feature-branches.yml @@ -0,0 +1,31 @@ +name: Feature Branch Tests + +on: + push: + branches: + - 'feature/**' + pull_request: + branches: + - master + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + test: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + cache: maven + + - name: Run unit tests + run: mvn -B test --file pom.xml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index cbb3902..d90a5d6 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -6,14 +6,22 @@ on: pull_request: branches: [master] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build with Maven - run: mvn -B package --file pom.xml + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + cache: maven + + - name: Run tests + run: mvn -B test --file pom.xml diff --git a/README.md b/README.md index c603dfc..80e6f95 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,42 @@ # What is dynamic-java-compiler? ![Build](https://github.com/raulgomis/dynamic-java-compiler/workflows/Build/badge.svg) -_Dynamic-java-compiler_ is a library that allows users to dynamically compile and execute any java source code. Writing dynamically executed Java applications require some boilerplate code: working with classloaders, compilation error handling, etc. The idea behind this library is to free you from this development and let you focus on your business logic. +_Dynamic-java-compiler_ is a library that allows users to dynamically compile and execute Java source code. Writing dynamically executed Java applications usually requires boilerplate code (working with classloaders, compilation error handling, etc.). This library removes that overhead so you can focus on business logic. -# How does it work? +## Java compatibility -The dynamic ompilation task is very simple with this library. Imagine we want to compile this source code introduced dynamically as a text string by the user: +This project is configured to compile using Java **25** (`maven.compiler.release=25`) and has tests updated for modern JDK APIs. + +## How does it work? + +The dynamic compilation task is very simple with this library. Imagine we want to compile this source code introduced dynamically as a text string by the user: ```java public class Test01 implements Runnable { - public void run() { - System.out.println("Hello World!"); - } + public void run() { + System.out.println("Hello World!"); + } } ``` -So simple, we just need to instantiate the compiler and compile the code: +Now instantiate the compiler and compile the code: ```java DynamicCompiler compiler = new DynamicCompiler<>(); // Read source code as String Class clazz = compiler.compile(null, "Test01", source); -final Runnable r; -try { - r = clazz.newInstance(); - r.run(); -} catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); -} +Runnable runnable = clazz.getDeclaredConstructor().newInstance(); +runnable.run(); ``` The final result will be: + ``` Hello World! ``` - ## Contribution You are welcome to contribute to the project using pull requests on GitHub. -If you find a bug or want to request a feature, please use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) of Github. +If you find a bug or want to request a feature, please use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) on GitHub. diff --git a/pom.xml b/pom.xml index ed9a419..169cef3 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ The MIT License https://github.com/raulgomis/dynamic-java-compiler/blob/master/LICENSE - + @@ -29,27 +29,15 @@ UTF-8 - 1.8 - 1.8 + 25 + 5.12.2 org.junit.jupiter - junit-jupiter-engine - 5.6.1 - test - - - org.junit.jupiter - junit-jupiter-params - 5.6.1 - test - - - org.junit.jupiter - junit-jupiter-api - 5.6.1 + junit-jupiter + ${junit.jupiter.version} test @@ -59,17 +47,15 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 - ${maven.compiler.source} - ${maven.compiler.target} + ${maven.compiler.release} - org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.4 diff --git a/src/main/java/com/raulgomis/djc/DynamicByteObject.java b/src/main/java/com/raulgomis/djc/DynamicByteObject.java index 248c055..8fd5146 100644 --- a/src/main/java/com/raulgomis/djc/DynamicByteObject.java +++ b/src/main/java/com/raulgomis/djc/DynamicByteObject.java @@ -1,11 +1,12 @@ package com.raulgomis.djc; -import javax.tools.SimpleJavaFileObject; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import javax.tools.SimpleJavaFileObject; public class DynamicByteObject extends SimpleJavaFileObject { - private ByteArrayOutputStream outputStream; + + private final ByteArrayOutputStream outputStream; DynamicByteObject(String name, Kind kind) { super(DynamicCompilerUtils.createURI(name), kind); diff --git a/src/main/java/com/raulgomis/djc/DynamicClassLoader.java b/src/main/java/com/raulgomis/djc/DynamicClassLoader.java index f075ada..24c0260 100644 --- a/src/main/java/com/raulgomis/djc/DynamicClassLoader.java +++ b/src/main/java/com/raulgomis/djc/DynamicClassLoader.java @@ -1,29 +1,28 @@ package com.raulgomis.djc; -import javax.tools.JavaFileObject.Kind; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.tools.JavaFileObject.Kind; public final class DynamicClassLoader extends ClassLoader { - private Map classes = new HashMap<>(); + private final Map classes = new ConcurrentHashMap<>(); DynamicClassLoader(ClassLoader parentClassLoader) { super(parentClassLoader); } void addClass(DynamicByteObject compiledObj) { - System.out.println("Compiled " + compiledObj.getName()); classes.put(compiledObj.getName(), compiledObj); } @Override public Class findClass(String className) throws ClassNotFoundException { - DynamicByteObject byteObject = classes.get(className); + final DynamicByteObject byteObject = classes.get(className); if (byteObject != null) { - byte[] bytes = byteObject.getBytes(); + final byte[] bytes = byteObject.getBytes(); return defineClass(className, bytes, 0, bytes.length); } @@ -31,15 +30,10 @@ public Class findClass(String className) throws ClassNotFoundException { } @Override - protected synchronized Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { - return super.loadClass(name, resolve); - } - - @Override - public InputStream getResourceAsStream(final String name) { + public InputStream getResourceAsStream(String name) { if (name.endsWith(Kind.CLASS.extension)) { - String qualifiedClassName = name.substring(0, name.length() - Kind.CLASS.extension.length()).replace('/', '.'); - DynamicByteObject file = classes.get(qualifiedClassName); + final String qualifiedClassName = name.substring(0, name.length() - Kind.CLASS.extension.length()).replace('/', '.'); + final DynamicByteObject file = classes.get(qualifiedClassName); if (file != null) { return new ByteArrayInputStream(file.getBytes()); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompiler.java b/src/main/java/com/raulgomis/djc/DynamicCompiler.java index 614f2db..ea562e9 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompiler.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompiler.java @@ -1,5 +1,6 @@ package com.raulgomis.djc; +import java.util.List; import javax.tools.DiagnosticCollector; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; @@ -7,15 +8,13 @@ import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import javax.tools.ToolProvider; -import java.util.Arrays; public final class DynamicCompiler { - private JavaCompiler compiler; - private DynamicExtendedFileManager dynamicExtendedFileManager; - private DynamicClassLoader classLoader; - - private DiagnosticCollector diagnostics; + private final JavaCompiler compiler; + private final DynamicExtendedFileManager dynamicExtendedFileManager; + private final DynamicClassLoader classLoader; + private final DiagnosticCollector diagnostics; public DynamicCompiler() throws DynamicCompilerException { compiler = ToolProvider.getSystemJavaCompiler(); @@ -26,31 +25,40 @@ public DynamicCompiler() throws DynamicCompilerException { classLoader = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); diagnostics = new DiagnosticCollector<>(); - StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null); + final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null); dynamicExtendedFileManager = new DynamicExtendedFileManager(standardFileManager, classLoader); } @SuppressWarnings("unchecked") public synchronized Class compile(String packageName, String className, String javaSource) throws DynamicCompilerException { try { + final String qualifiedClassName = DynamicCompilerUtils.getQualifiedClassName(packageName, className); + final DynamicStringObject sourceObj = new DynamicStringObject(className, javaSource); - String qualifiedClassName = DynamicCompilerUtils.getQualifiedClassName(packageName, className); - DynamicStringObject sourceObj = new DynamicStringObject(className, javaSource); - - dynamicExtendedFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, - className + JavaFileObject.Kind.SOURCE.extension, sourceObj); + dynamicExtendedFileManager.putFileForInput( + StandardLocation.SOURCE_PATH, + packageName, + className + JavaFileObject.Kind.SOURCE.extension, + sourceObj + ); - CompilationTask task = compiler.getTask(null, dynamicExtendedFileManager, diagnostics, null, null, Arrays.asList(sourceObj)); - boolean result = task.call(); + final CompilationTask task = compiler.getTask( + null, + dynamicExtendedFileManager, + diagnostics, + null, + null, + List.of(sourceObj) + ); - if (!result) { + if (!task.call()) { throw new DynamicCompilerException("Compilation failure", diagnostics.getDiagnostics()); } dynamicExtendedFileManager.close(); - return (Class) classLoader.loadClass(qualifiedClassName); - + } catch (DynamicCompilerException exception) { + throw exception; } catch (Exception exception) { throw new DynamicCompilerException(exception, diagnostics.getDiagnostics()); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java index b6eb814..c0c3709 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java @@ -1,34 +1,33 @@ package com.raulgomis.djc; +import java.util.List; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; -import java.util.List; public final class DynamicCompilerException extends Exception { - private List> diagnostics; + private final List> diagnostics; public DynamicCompilerException(String message) { super(message); + this.diagnostics = List.of(); } public DynamicCompilerException(String message, List> diagnostics) { super(message); - this.diagnostics = diagnostics; + this.diagnostics = diagnostics == null ? List.of() : List.copyOf(diagnostics); } public DynamicCompilerException(Throwable e, List> diagnostics) { super(e); - this.diagnostics = diagnostics; + this.diagnostics = diagnostics == null ? List.of() : List.copyOf(diagnostics); } public String getDiagnosticsError() { - StringBuilder sb = new StringBuilder(); - if(diagnostics != null) { - diagnostics.forEach(diagnostic -> sb.append(String.format("Error on line %d: %s\n", - diagnostic.getLineNumber(), - diagnostic.getMessage(null)))); - } + final StringBuilder sb = new StringBuilder(); + diagnostics.forEach(diagnostic -> sb.append( + String.format("Error on line %d: %s%n", diagnostic.getLineNumber(), diagnostic.getMessage(null)) + )); return sb.toString(); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java b/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java index 0356b31..e53c28b 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java @@ -1,31 +1,29 @@ package com.raulgomis.djc; -import javax.tools.JavaFileObject.Kind; import java.net.URI; import java.net.URISyntaxException; +import javax.tools.JavaFileObject.Kind; public final class DynamicCompilerUtils { - private DynamicCompilerUtils() { + public static final String EMPTY = ""; + private DynamicCompilerUtils() { } - public static final String EMPTY = ""; - static URI createURI(String str) { try { return new URI(str); } catch (URISyntaxException e) { - throw new RuntimeException(e); + throw new IllegalArgumentException("Invalid URI: " + str, e); } } static String getQualifiedClassName(String packageName, String className) { if (isEmpty(packageName)) { return className; - } else { - return packageName + "." + className; } + return packageName + "." + className; } static String getClassNameWithExt(String className) { @@ -33,7 +31,6 @@ static String getClassNameWithExt(String className) { } static boolean isEmpty(String str) { - return str == null || str.length() == 0; + return str == null || str.isEmpty(); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java b/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java index e87340b..7782c53 100644 --- a/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java +++ b/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java @@ -1,19 +1,19 @@ package com.raulgomis.djc; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardLocation; -import java.io.IOException; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; public final class DynamicExtendedFileManager extends ForwardingJavaFileManager { - private DynamicClassLoader classLoader; + private final DynamicClassLoader classLoader; private final Map fileObjects = new HashMap<>(); DynamicExtendedFileManager(JavaFileManager fileManager, DynamicClassLoader classLoader) { @@ -23,9 +23,9 @@ public final class DynamicExtendedFileManager extends ForwardingJavaFileManager< @Override public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { - FileObject o = fileObjects.get(uri(location, packageName, relativeName)); - if (o != null) { - return o; + final FileObject fileObject = fileObjects.get(uri(location, packageName, relativeName)); + if (fileObject != null) { + return fileObject; } return super.getFileForInput(location, packageName, relativeName); } @@ -38,11 +38,9 @@ private URI uri(Location location, String packageName, String relativeName) { return DynamicCompilerUtils.createURI(location.getName() + '/' + packageName + '/' + relativeName); } - @Override - public JavaFileObject getJavaFileForOutput(Location location, - String qualifiedName, Kind kind, FileObject outputFile) { - DynamicByteObject dynamicByteObject = new DynamicByteObject(qualifiedName, kind); + public JavaFileObject getJavaFileForOutput(Location location, String qualifiedName, Kind kind, FileObject outputFile) { + final DynamicByteObject dynamicByteObject = new DynamicByteObject(qualifiedName, kind); classLoader.addClass(dynamicByteObject); return dynamicByteObject; } @@ -53,10 +51,10 @@ public ClassLoader getClassLoader(JavaFileManager.Location location) { } @Override - public String inferBinaryName(Location loc, JavaFileObject file) { + public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof DynamicByteObject) { return file.getName(); } - return super.inferBinaryName(loc, file); + return super.inferBinaryName(location, file); } } diff --git a/src/main/java/com/raulgomis/djc/DynamicStringObject.java b/src/main/java/com/raulgomis/djc/DynamicStringObject.java index c7c5af7..d2b1059 100644 --- a/src/main/java/com/raulgomis/djc/DynamicStringObject.java +++ b/src/main/java/com/raulgomis/djc/DynamicStringObject.java @@ -3,6 +3,7 @@ import javax.tools.SimpleJavaFileObject; public class DynamicStringObject extends SimpleJavaFileObject { + private final String source; DynamicStringObject(String name, String source) { diff --git a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java index 791cbb7..42c7d55 100644 --- a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java +++ b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java @@ -5,57 +5,41 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; class DynamicCompilerTest { @Test - void test01() { + void compilesAndInstantiatesSimpleRunnable() throws Exception { CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - Class clazz = compilerExecutor.compile("Test01.java"); - final Runnable r; - try { - r = clazz.newInstance(); - r.run(); - } catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); - } - assertNotNull(clazz); - } catch (DynamicCompilerException e) { - e.printStackTrace(); - fail(); - } + + Class clazz = compilerExecutor.compile("Test01.java"); + Runnable runnable = clazz.getDeclaredConstructor().newInstance(); + + runnable.run(); + assertNotNull(clazz); } @Test - void test02() { + void returnsReadableDiagnosticsForCompilationFailure() { CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - compilerExecutor.compile("Test02.java"); - fail(); - } catch (DynamicCompilerException e) { - assertEquals("Error on line 3: ';' expected\n", - e.getDiagnosticsError()); - } + + DynamicCompilerException exception = assertThrows( + DynamicCompilerException.class, + () -> compilerExecutor.compile("Test02.java") + ); + + assertEquals("Error on line 3: ';' expected\n", exception.getDiagnosticsError()); } @Test - void test03() { - CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - Class clazz = compilerExecutor.compile("Test03.java"); - final Runnable r; - try { - r = clazz.newInstance(); - r.run(); - } catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); - } - assertNotNull(clazz); - } catch (DynamicCompilerException e) { - e.printStackTrace(); - fail(); - } + void compilesRunnableWithImports() throws Exception { + CompilerExecutor compilerExecutor = new CompilerExecutor(); + + Class clazz = compilerExecutor.compile("Test03.java"); + Runnable runnable = clazz.getDeclaredConstructor().newInstance(); + + runnable.run(); + assertNotNull(clazz); } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java b/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java index b3e1ce0..b618e92 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java @@ -3,15 +3,11 @@ import com.raulgomis.djc.DynamicCompiler; import com.raulgomis.djc.DynamicCompilerException; -/** - * @author rgomis - */ public class CompilerExecutor { - public Class compile(final String className) throws DynamicCompilerException { + public Class compile(String className) throws DynamicCompilerException { DynamicCompiler compiler = new DynamicCompiler<>(); CompilerTestInput compilerTestInput = CompilerTestUtils.getSource(className); - Class clazz = compiler.compile(null, compilerTestInput.getClassName(), compilerTestInput.getSource()); - return clazz; + return compiler.compile(null, compilerTestInput.getClassName(), compilerTestInput.getSource()); } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java b/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java index 701ac0f..684c78a 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java @@ -1,7 +1,6 @@ package com.raulgomis.djc.utils; import com.raulgomis.djc.DynamicCompilerUtils; - import java.io.Serializable; import java.util.Objects; @@ -11,9 +10,13 @@ class CompilerTestInput implements Serializable { static final CompilerTestInput EMPTY = new CompilerTestInput(DynamicCompilerUtils.EMPTY, DynamicCompilerUtils.EMPTY); - private String className; + private final String className; + private final String source; - private String source; + CompilerTestInput(String className, String source) { + this.className = className; + this.source = source; + } String getClassName() { return className; @@ -23,19 +26,16 @@ String getSource() { return source; } - CompilerTestInput(String className, String source) { - this.className = className; - this.source = source; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } CompilerTestInput that = (CompilerTestInput) o; - return Objects.equals(className, that.className) && - Objects.equals(source, that.source); + return Objects.equals(className, that.className) && Objects.equals(source, that.source); } @Override @@ -46,8 +46,8 @@ public int hashCode() { @Override public String toString() { return "CompilerTestInput{" + - "className='" + className + '\'' + - ", source='" + source + '\'' + - '}'; + "className='" + className + '\'' + + ", source='" + source + '\'' + + '}'; } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java b/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java index 96d5dba..0089ad7 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java @@ -1,7 +1,6 @@ package com.raulgomis.djc.utils; import com.raulgomis.djc.DynamicCompilerUtils; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -10,29 +9,23 @@ import java.nio.file.Path; import java.nio.file.Paths; -/** - * @author rgomis - */ class CompilerTestUtils { private static final String SPLIT_EXPRESSION = "\\."; private CompilerTestUtils() { - } static CompilerTestInput getSource(String fileName) { try { - URI uri = DynamicCompilerUtils.class.getResource("/" + fileName).toURI(); + URI uri = java.util.Objects.requireNonNull(DynamicCompilerUtils.class.getResource("/" + fileName)).toURI(); Path path = Paths.get(uri); - byte[] result = Files.readAllBytes(path); - String source = new String(result, StandardCharsets.UTF_8); + String source = Files.readString(path, StandardCharsets.UTF_8); String className = path.getFileName().toString().split(SPLIT_EXPRESSION)[0]; return new CompilerTestInput(className, source); } catch (URISyntaxException | IOException e) { - e.printStackTrace(); return CompilerTestInput.EMPTY; } }