diff --git a/README.adoc b/README.adoc index 638ebdc..e0e5b66 100644 --- a/README.adoc +++ b/README.adoc @@ -45,23 +45,22 @@ Class aClass = CompilerUtils.CACHED_COMPILER.loadFromJava(className, javaCode Runnable runner = (Runnable) aClass.newInstance(); runner.run(); ``` - + I suggest making your class implement a KnownInterface of your choice as this will allow you to call/manipulate instances of you generated class. -Another more hacky way is to use this to override a class, provided it hasn't been loaded already. -This means you can redefine an existing class and provide the methods and fields used match, -you have compiler redefine a class and code already compiled to use the class will still work. +Another more hacky way is to use this to override a class, provided it hasn't been loaded already. +This means you can redefine an existing class and provide the methods and fields used match, you have compiler redefine a class and code already compiled to use the class will still work. == Using the CachedCompiler. In this example, you can configure the compiler to write the files to a specific directory when you are in debug mode. - + ```java private static final CachedCompiler JCC = CompilerUtils.DEBUGGING ? new CachedCompiler(new File(parent, "src/test/java"), new File(parent, "target/compiled")) : CompilerUtils.CACHED_COMPILER; ``` - + By selecting the src directory to match where your IDE looks for those files, it will allow your debugger to set into the code you have generated at runtime. Note: you may need to delete these files if you want to regenerate them. diff --git a/pom.xml b/pom.xml index accd6ac..7b6b18c 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,8 @@ ~ limitations under the License. --> - + 4.0.0 @@ -75,14 +76,19 @@ slf4j-simple test + + org.junit.jupiter + junit-jupiter-api + test + - openhft - https://sonarcloud.io + openhft + https://sonarcloud.io - + @@ -212,17 +218,17 @@ - - - chronicle-enterprise-release - https://nexus.chronicle.software/content/repositories/releases - - - chronicle-enterprise-snapshots - https://nexus.chronicle.software/content/repositories/snapshots - - - + + + chronicle-enterprise-release + https://nexus.chronicle.software/content/repositories/releases + + + chronicle-enterprise-snapshots + https://nexus.chronicle.software/content/repositories/snapshots + + + scm:git:git@github.com:OpenHFT/Java-Runtime-Compiler.git scm:git:git@github.com:OpenHFT/Java-Runtime-Compiler.git diff --git a/src/main/java/net/openhft/compiler/CachedCompiler.java b/src/main/java/net/openhft/compiler/CachedCompiler.java index ca64f0b..731f0ed 100644 --- a/src/main/java/net/openhft/compiler/CachedCompiler.java +++ b/src/main/java/net/openhft/compiler/CachedCompiler.java @@ -38,35 +38,59 @@ import static net.openhft.compiler.CompilerUtils.*; +/** + * This class handles the caching and compilation of Java source code. + * It maintains a cache of loaded classes and provides methods to compile + * Java source code and load classes dynamically. + */ @SuppressWarnings("StaticNonFinalField") public class CachedCompiler implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(CachedCompiler.class); private static final PrintWriter DEFAULT_WRITER = new PrintWriter(System.err); private static final List DEFAULT_OPTIONS = Arrays.asList("-g", "-nowarn"); + // Map to store loaded classes, synchronized to handle concurrent access private final Map>> loadedClassesMap = Collections.synchronizedMap(new WeakHashMap<>()); + // Map to store file managers, synchronized to handle concurrent access private final Map fileManagerMap = Collections.synchronizedMap(new WeakHashMap<>()); public Function fileManagerOverride; @Nullable - private final File sourceDir; + private final File sourceDir; // Directory for source files @Nullable - private final File classDir; + private final File classDir; // Directory for class files @NotNull - private final List options; + private final List options; // Compilation options + // Map to store Java file objects for compilation private final ConcurrentMap javaFileObjects = new ConcurrentHashMap<>(); + /** + * Constructor to initialize CachedCompiler with source and class directories, and default options. + * + * @param sourceDir The directory for source files + * @param classDir The directory for class files + */ public CachedCompiler(@Nullable File sourceDir, @Nullable File classDir) { this(sourceDir, classDir, DEFAULT_OPTIONS); } + /** + * Constructor to initialize CachedCompiler with source and class directories, and custom options. + * + * @param sourceDir The directory for source files + * @param classDir The directory for class files + * @param options The compilation options + */ public CachedCompiler(@Nullable File sourceDir, @Nullable File classDir, @NotNull List options) { this.sourceDir = sourceDir; this.classDir = classDir; this.options = options; } + /** + * Closes the CachedCompiler and releases resources associated with the file managers. + */ public void close() { try { for (MyJavaFileManager fileManager : fileManagerMap.values()) { @@ -77,21 +101,55 @@ public void close() { } } + /** + * Loads a class from the provided Java source code. + * + * @param className The name of the class to be loaded + * @param javaCode The Java source code of the class + * @return The loaded class + * @throws ClassNotFoundException if the class cannot be found + */ public Class loadFromJava(@NotNull String className, @NotNull String javaCode) throws ClassNotFoundException { return loadFromJava(getClass().getClassLoader(), className, javaCode, DEFAULT_WRITER); } + /** + * Loads a class from the provided Java source code using the specified class loader. + * + * @param classLoader The class loader to be used + * @param className The name of the class to be loaded + * @param javaCode The Java source code of the class + * @return The loaded class + * @throws ClassNotFoundException if the class cannot be found + */ public Class loadFromJava(@NotNull ClassLoader classLoader, - @NotNull String className, - @NotNull String javaCode) throws ClassNotFoundException { + @NotNull String className, + @NotNull String javaCode) throws ClassNotFoundException { return loadFromJava(classLoader, className, javaCode, DEFAULT_WRITER); } + /** + * Compiles the provided Java source code and returns a map of class names to byte arrays. + * + * @param className The name of the class to be compiled + * @param javaCode The Java source code of the class + * @param fileManager The file manager to be used for compilation + * @return A map of class names to byte arrays + */ @NotNull Map compileFromJava(@NotNull String className, @NotNull String javaCode, MyJavaFileManager fileManager) { return compileFromJava(className, javaCode, DEFAULT_WRITER, fileManager); } + /** + * Compiles the provided Java source code and returns a map of class names to byte arrays. + * + * @param className The name of the class to be compiled + * @param javaCode The Java source code of the class + * @param writer The PrintWriter to be used for logging + * @param fileManager The file manager to be used for compilation + * @return A map of class names to byte arrays + */ @NotNull Map compileFromJava(@NotNull String className, @NotNull String javaCode, @@ -99,6 +157,7 @@ Map compileFromJava(@NotNull String className, MyJavaFileManager fileManager) { Iterable compilationUnits; if (sourceDir != null) { + // Write source file to disk if sourceDir is specified String filename = className.replaceAll("\\.", '\\' + File.separator) + ".java"; File file = new File(sourceDir, filename); writeText(file, javaCode); @@ -107,10 +166,11 @@ Map compileFromJava(@NotNull String className, compilationUnits = s_standardJavaFileManager.getJavaFileObjects(file); } else { + // Use in-memory Java file object if sourceDir is not specified javaFileObjects.put(className, new JavaSourceFromString(className, javaCode)); compilationUnits = new ArrayList<>(javaFileObjects.values()); // To prevent CME from compiler code } - // reuse the same file manager to allow caching of jar files + // Reuse the same file manager to allow caching of jar files boolean ok = s_compiler.getTask(writer, fileManager, new DiagnosticListener() { @Override public void report(Diagnostic diagnostic) { @@ -121,23 +181,34 @@ public void report(Diagnostic diagnostic) { }, options, null, compilationUnits).call(); if (!ok) { - // compilation error, so we want to exclude this file from future compilation passes + // Compilation error, so we want to exclude this file from future compilation passes if (sourceDir == null) javaFileObjects.remove(className); - // nothing to return due to compiler error + // Nothing to return due to compiler error return Collections.emptyMap(); } else { + // Return compiled class byte arrays Map result = fileManager.getAllBuffers(); return result; } } + /** + * Loads a class from the provided Java source code using the specified class loader and writer. + * + * @param classLoader The class loader to be used + * @param className The name of the class to be loaded + * @param javaCode The Java source code of the class + * @param writer The PrintWriter to be used for logging + * @return The loaded class + * @throws ClassNotFoundException if the class cannot be found + */ public Class loadFromJava(@NotNull ClassLoader classLoader, - @NotNull String className, - @NotNull String javaCode, - @Nullable PrintWriter writer) throws ClassNotFoundException { + @NotNull String className, + @NotNull String javaCode, + @Nullable PrintWriter writer) throws ClassNotFoundException { Class clazz = null; Map> loadedClasses; synchronized (loadedClassesMap) { @@ -166,6 +237,7 @@ public Class loadFromJava(@NotNull ClassLoader classLoader, } byte[] bytes = entry.getValue(); if (classDir != null) { + // Write compiled class file to disk if classDir is specified String filename = className2.replaceAll("\\.", '\\' + File.separator) + ".class"; boolean changed = writeBytes(new File(classDir, filename), bytes); if (changed) { @@ -191,7 +263,14 @@ public Class loadFromJava(@NotNull ClassLoader classLoader, return clazz; } - private @NotNull MyJavaFileManager getFileManager(StandardJavaFileManager fm) { + /** + * Gets a MyJavaFileManager instance from the provided StandardJavaFileManager. + * + * @param fm The StandardJavaFileManager instance + * @return The MyJavaFileManager instance + */ + @NotNull + public MyJavaFileManager getFileManager(StandardJavaFileManager fm) { return fileManagerOverride != null ? fileManagerOverride.apply(fm) : new MyJavaFileManager(fm); diff --git a/src/main/java/net/openhft/compiler/CloseableByteArrayOutputStream.java b/src/main/java/net/openhft/compiler/CloseableByteArrayOutputStream.java index c30440f..d6b6e09 100644 --- a/src/main/java/net/openhft/compiler/CloseableByteArrayOutputStream.java +++ b/src/main/java/net/openhft/compiler/CloseableByteArrayOutputStream.java @@ -21,14 +21,28 @@ import java.io.ByteArrayOutputStream; import java.util.concurrent.CompletableFuture; +/** + * This class extends ByteArrayOutputStream and provides a CompletableFuture + * that completes when the stream is closed. + */ public class CloseableByteArrayOutputStream extends ByteArrayOutputStream { + // CompletableFuture that completes when the stream is closed private final CompletableFuture closeFuture = new CompletableFuture<>(); + /** + * Closes this output stream and completes the closeFuture. + */ @Override public void close() { + // Complete the closeFuture to signal that the stream has been closed closeFuture.complete(null); } + /** + * Returns the CompletableFuture that completes when the stream is closed. + * + * @return The CompletableFuture that completes when the stream is closed + */ public CompletableFuture closeFuture() { return closeFuture; } diff --git a/src/main/java/net/openhft/compiler/CompilerUtils.java b/src/main/java/net/openhft/compiler/CompilerUtils.java index 1bbcc39..3054044 100644 --- a/src/main/java/net/openhft/compiler/CompilerUtils.java +++ b/src/main/java/net/openhft/compiler/CompilerUtils.java @@ -37,7 +37,7 @@ import java.util.Arrays; /** - * This class support loading and debugging Java Classes dynamically. + * This class supports loading and debugging Java Classes dynamically. */ public enum CompilerUtils { ; // none @@ -73,12 +73,21 @@ public enum CompilerUtils { reset(); } - private static boolean isDebug() { + /** + * Checks if the JVM is running in debug mode. + * + * @return true if the JVM is in debug mode, false otherwise + */ + public static boolean isDebug() { String inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); return inputArguments.contains("-Xdebug") || inputArguments.contains("-agentlib:jdwp="); } - private static void reset() { + /** + * Resets the Java compiler instance. Attempts to load the system Java compiler + * or the JavacTool if the system compiler is not available. + */ + public static void reset() { s_compiler = ToolProvider.getSystemJavaCompiler(); if (s_compiler == null) { try { @@ -92,35 +101,36 @@ private static void reset() { } /** - * Load a java class file from the classpath or local file system. + * Loads a Java class file from the classpath or local file system. * - * @param className expected class name of the outer class. - * @param resourceName as the full file name with extension. - * @return the outer class loaded. - * @throws IOException the resource could not be loaded. - * @throws ClassNotFoundException the class name didn't match or failed to initialise. + * @param className The expected class name of the outer class + * @param resourceName The full file name with extension + * @return The outer class loaded + * @throws IOException If the resource could not be loaded + * @throws ClassNotFoundException If the class name didn't match or failed to initialize */ public static Class loadFromResource(@NotNull String className, @NotNull String resourceName) throws IOException, ClassNotFoundException { return loadFromJava(className, readText(resourceName)); } /** - * Load a java class from text. + * Loads a Java class from the provided Java source code. * - * @param className expected class name of the outer class. - * @param javaCode to compile and load. - * @return the outer class loaded. - * @throws ClassNotFoundException the class name didn't match or failed to initialise. + * @param className The expected class name of the outer class + * @param javaCode The Java source code to compile and load + * @return The outer class loaded + * @throws ClassNotFoundException If the class name didn't match or failed to initialize */ private static Class loadFromJava(@NotNull String className, @NotNull String javaCode) throws ClassNotFoundException { return CACHED_COMPILER.loadFromJava(Thread.currentThread().getContextClassLoader(), className, javaCode); } /** - * Add a directory to the class path for compiling. This can be required with custom + * Adds a directory to the classpath for compiling. This can be required with custom + * libraries or dependencies. * - * @param dir to add. - * @return whether the directory was found, if not it is not added either. + * @param dir The directory to add + * @return Whether the directory was found and added successfully */ public static boolean addClassPath(@NotNull String dir) { File file = new File(dir); @@ -131,6 +141,7 @@ public static boolean addClassPath(@NotNull String dir) { } catch (IOException ignored) { path = file.getAbsolutePath(); } + // Add the directory to the classpath if not already present if (!Arrays.asList(System.getProperty(JAVA_CLASS_PATH).split(File.pathSeparator)).contains(path)) System.setProperty(JAVA_CLASS_PATH, System.getProperty(JAVA_CLASS_PATH) + File.pathSeparator + path); @@ -142,10 +153,10 @@ public static boolean addClassPath(@NotNull String dir) { } /** - * Define a class for byte code. + * Defines a class from the provided byte code. * - * @param className expected to load. - * @param bytes of the byte code. + * @param className The expected class name + * @param bytes The byte code of the class */ public static void defineClass(@NotNull String className, @NotNull byte[] bytes) { defineClass(Thread.currentThread().getContextClassLoader(), className, bytes); @@ -169,6 +180,13 @@ public static Class defineClass(@Nullable ClassLoader classLoader, @NotNull S } } + /** + * Reads the text content from a resource. + * + * @param resourceName The name of the resource + * @return The text content of the resource + * @throws IOException If the resource cannot be read + */ private static String readText(@NotNull String resourceName) throws IOException { if (resourceName.startsWith("=")) return resourceName.substring(1); @@ -185,8 +203,14 @@ private static String readText(@NotNull String resourceName) throws IOException return sw.toString(); } + /** + * Decodes a byte array into a UTF-8 string. + * + * @param bytes The byte array to decode + * @return The decoded string + */ @NotNull - private static String decodeUTF8(@NotNull byte[] bytes) { + public static String decodeUTF8(@NotNull byte[] bytes) { try { return new String(bytes, UTF_8.name()); } catch (UnsupportedEncodingException e) { @@ -194,9 +218,15 @@ private static String decodeUTF8(@NotNull byte[] bytes) { } } + /** + * Reads the bytes from a file. + * + * @param file The file to read + * @return The bytes read from the file, or null if the file does not exist + */ @Nullable @SuppressWarnings("ReturnOfNull") - private static byte[] readBytes(@NotNull File file) { + public static byte[] readBytes(@NotNull File file) { if (!file.exists()) return null; long len = file.length(); if (len > Runtime.getRuntime().totalMemory() / 10) @@ -215,7 +245,12 @@ private static byte[] readBytes(@NotNull File file) { return bytes; } - private static void close(@Nullable Closeable closeable) { + /** + * Closes a Closeable object, suppressing any IOException that occurs. + * + * @param closeable The Closeable object to close + */ + public static void close(@Nullable Closeable closeable) { if (closeable != null) try { closeable.close(); @@ -224,12 +259,25 @@ private static void close(@Nullable Closeable closeable) { } } + /** + * Writes a string as UTF-8 text to a file. + * + * @param file The file to write to + * @param text The text to write + * @return true if the file was written successfully, false otherwise + */ public static boolean writeText(@NotNull File file, @NotNull String text) { return writeBytes(file, encodeUTF8(text)); } + /** + * Encodes a string into a byte array using UTF-8 encoding. + * + * @param text The string to encode + * @return The encoded byte array + */ @NotNull - private static byte[] encodeUTF8(@NotNull String text) { + public static byte[] encodeUTF8(@NotNull String text) { try { return text.getBytes(UTF_8.name()); } catch (UnsupportedEncodingException e) { @@ -237,6 +285,15 @@ private static byte[] encodeUTF8(@NotNull String text) { } } + /** + * Writes a byte array to a file. If the file already exists and its contents + * are identical to the byte array, the file is not modified. If the contents + * differ, the file is backed up before being overwritten. + * + * @param file The file to write to + * @param bytes The byte array to write + * @return true if the file was written successfully, false otherwise + */ public static boolean writeBytes(@NotNull File file, @NotNull byte[] bytes) { File parentDir = file.getParentFile(); if (!parentDir.isDirectory() && !parentDir.mkdirs()) @@ -266,8 +323,17 @@ public static boolean writeBytes(@NotNull File file, @NotNull byte[] bytes) { return true; } + /** + * Gets an InputStream for a given filename. The method tries to load the file + * from the classpath using the context class loader. If the file is not found, + * it falls back to loading it from the filesystem. + * + * @param filename The name of the file to load + * @return The InputStream for the file + * @throws FileNotFoundException If the file cannot be found + */ @NotNull - private static InputStream getInputStream(@NotNull String filename) throws FileNotFoundException { + public static InputStream getInputStream(@NotNull String filename) throws FileNotFoundException { if (filename.isEmpty()) throw new IllegalArgumentException("The file name cannot be empty."); if (filename.charAt(0) == '=') return new ByteArrayInputStream(encodeUTF8(filename.substring(1))); ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); diff --git a/src/main/java/net/openhft/compiler/JavaSourceFromString.java b/src/main/java/net/openhft/compiler/JavaSourceFromString.java index 35b9eca..64bb1e3 100644 --- a/src/main/java/net/openhft/compiler/JavaSourceFromString.java +++ b/src/main/java/net/openhft/compiler/JavaSourceFromString.java @@ -23,7 +23,8 @@ import javax.tools.SimpleJavaFileObject; import java.net.URI; -/* A file object used to represent source coming from a string. +/** + * A file object used to represent source coming from a string. */ class JavaSourceFromString extends SimpleJavaFileObject { /** @@ -38,14 +39,22 @@ class JavaSourceFromString extends SimpleJavaFileObject { * @param code the source code for the compilation unit represented by this file object */ JavaSourceFromString(@NotNull String name, String code) { + // Create a URI for the source file. The Kind.SOURCE indicates that this is a source file. super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); this.code = code; } + /** + * Returns the source code content of this file object. + * + * @param ignoreEncodingErrors If true, encoding errors are ignored + * @return The source code as a CharSequence + */ @SuppressWarnings("RefusedBequest") @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { + // Return the source code content return code; } } diff --git a/src/main/java/net/openhft/compiler/MyJavaFileManager.java b/src/main/java/net/openhft/compiler/MyJavaFileManager.java index 9ce7a13..fea6535 100644 --- a/src/main/java/net/openhft/compiler/MyJavaFileManager.java +++ b/src/main/java/net/openhft/compiler/MyJavaFileManager.java @@ -39,6 +39,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +/** + * This class is a custom implementation of JavaFileManager used for managing Java file objects + * and class loaders. It provides additional functionalities to handle in-memory compilation. + */ public class MyJavaFileManager implements JavaFileManager { private static final Logger LOG = LoggerFactory.getLogger(MyJavaFileManager.class); private final static Unsafe unsafe; @@ -67,46 +71,117 @@ public class MyJavaFileManager implements JavaFileManager { // synchronizing due to ConcurrentModificationException private final Map buffers = Collections.synchronizedMap(new LinkedHashMap<>()); + /** + * Constructs a MyJavaFileManager with the specified StandardJavaFileManager. + * + * @param fileManager The StandardJavaFileManager to be used + */ public MyJavaFileManager(StandardJavaFileManager fileManager) { this.fileManager = fileManager; } + /** + * Lists the locations for modules. This method is synchronized due to potential thread safety issues. + * + * @param location The location to list modules for + * @return An iterable of sets of locations + */ // Apparently, this method might not be thread-safe. // See https://github.com/OpenHFT/Java-Runtime-Compiler/issues/85 public synchronized Iterable> listLocationsForModules(final Location location) { return invokeNamedMethodIfAvailable(location, "listLocationsForModules"); } + /** + * Infers the module name for a given location. This method is synchronized due to potential thread safety issues. + * + * @param location The location to infer the module name for + * @return The inferred module name + */ // Apparently, this method might not be thread-safe. // See https://github.com/OpenHFT/Java-Runtime-Compiler/issues/85 public synchronized String inferModuleName(final Location location) { return invokeNamedMethodIfAvailable(location, "inferModuleName"); } + /** + * Gets the class loader for a given location. + * + * @param location The location to get the class loader for + * @return The class loader for the location + */ public ClassLoader getClassLoader(Location location) { return fileManager.getClassLoader(location); } + /** + * Lists JavaFileObjects for a given location, package name, and kind. This method is synchronized due to potential + * thread safety issues. + * + * @param location The location to list JavaFileObjects for + * @param packageName The package name to list JavaFileObjects for + * @param kinds The kinds of JavaFileObjects to list + * @param recurse Whether to recurse into subdirectories + * @return An iterable of JavaFileObjects + * @throws IOException If an I/O error occurs + */ public synchronized Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { return fileManager.list(location, packageName, kinds, recurse); } + /** + * Infers the binary name for a given JavaFileObject. + * + * @param location The location of the file + * @param file The JavaFileObject to infer the binary name for + * @return The binary name of the file + */ public String inferBinaryName(Location location, JavaFileObject file) { return fileManager.inferBinaryName(location, file); } + /** + * Checks if two FileObjects refer to the same file. + * + * @param a The first FileObject + * @param b The second FileObject + * @return true if the FileObjects refer to the same file, false otherwise + */ public boolean isSameFile(FileObject a, FileObject b) { return fileManager.isSameFile(a, b); } + /** + * Handles an option for the file manager. This method is synchronized due to potential + * thread safety issues. + * + * @param current The current option to handle + * @param remaining The remaining options iterator + * @return true if the option was handled, false otherwise + */ public synchronized boolean handleOption(String current, Iterator remaining) { return fileManager.handleOption(current, remaining); } + /** + * Checks if a given location is supported by the file manager. + * + * @param location The location to check + * @return true if the location is supported, false otherwise + */ public boolean hasLocation(Location location) { return fileManager.hasLocation(location); } + /** + * Gets a JavaFileObject for input at the specified location and class name. + * + * @param location The location to get the JavaFileObject for + * @param className The class name of the JavaFileObject + * @param kind The kind of the JavaFileObject + * @return The JavaFileObject for input + * @throws IOException If an I/O error occurs + */ public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException { if (location == StandardLocation.CLASS_OUTPUT) { @@ -129,6 +204,15 @@ public InputStream openInputStream() { return fileManager.getJavaFileForInput(location, className, kind); } + /** + * Gets a JavaFileObject for output at the specified location and class name. + * + * @param location The location to get the JavaFileObject for + * @param className The class name of the JavaFileObject + * @param kind The kind of the JavaFileObject + * @param sibling A sibling file object + * @return The JavaFileObject for output + */ @NotNull public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) { return new SimpleJavaFileObject(URI.create(className), kind) { @@ -146,30 +230,71 @@ public OutputStream openOutputStream() { }; } + /** + * Gets a FileObject for input at the specified location, package name, and relative name. + * + * @param location The location to get the FileObject for + * @param packageName The package name of the FileObject + * @param relativeName The relative name of the FileObject + * @return The FileObject for input + * @throws IOException If an I/O error occurs + */ public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { return fileManager.getFileForInput(location, packageName, relativeName); } + /** + * Gets a FileObject for output at the specified location, package name, and relative name. + * + * @param location The location to get the FileObject for + * @param packageName The package name of the FileObject + * @param relativeName The relative name of the FileObject + * @param sibling A sibling file object + * @return The FileObject for output + * @throws IOException If an I/O error occurs + */ public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException { return fileManager.getFileForOutput(location, packageName, relativeName, sibling); } + /** + * Flushes the file manager. This implementation does nothing. + */ public void flush() { // Do nothing } + /** + * Closes the file manager. + * + * @throws IOException If an I/O error occurs + */ public void close() throws IOException { fileManager.close(); } + /** + * Checks if the specified option is supported by the file manager. + * + * @param option The option to check + * @return The number of arguments the option takes, or -1 if the option is not supported + */ public int isSupportedOption(String option) { return fileManager.isSupportedOption(option); } + /** + * Clears the buffers used for storing compiled class byte code. + */ public void clearBuffers() { buffers.clear(); } + /** + * Gets all buffers containing compiled class byte code. + * + * @return A map of class names to byte arrays containing the compiled class byte code + */ @NotNull public Map getAllBuffers() { Map ret = new LinkedHashMap<>(buffers.size() * 2); @@ -203,6 +328,14 @@ public Map getAllBuffers() { return ret; } + /** + * Invokes a named method on the file manager if it is available. + * + * @param location The location to pass to the method + * @param name The name of the method to invoke + * @param The return type of the method + * @return The result of invoking the method + */ @SuppressWarnings("unchecked") private T invokeNamedMethodIfAvailable(final Location location, final String name) { final Method[] methods = fileManager.getClass().getDeclaredMethods(); diff --git a/src/main/java/net/openhft/compiler/internal/package-info.java b/src/main/java/net/openhft/compiler/internal/package-info.java index 8f3935d..e69de29 100644 --- a/src/main/java/net/openhft/compiler/internal/package-info.java +++ b/src/main/java/net/openhft/compiler/internal/package-info.java @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This package and any and all sub-packages contains strictly internal classes for this Chronicle library. - * Internal classes shall never be used directly. - *

- * Specifically, the following actions (including, but not limited to) are not allowed - * on internal classes and packages: - *

    - *
  • Casting to
  • - *
  • Reflection of any kind
  • - *
  • Explicit Serialize/deserialize
  • - *
- *

- * The classes in this package and any sub-package are subject to - * changes at any time for any reason. - */ -package net.openhft.compiler.internal; diff --git a/src/main/java/net/openhft/compiler/package-info.java b/src/main/java/net/openhft/compiler/package-info.java new file mode 100644 index 0000000..1880a17 --- /dev/null +++ b/src/main/java/net/openhft/compiler/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides classes and interfaces for dynamic Java code compilation and class loading. + * The main components of this package are classes that enable on-the-fly compilation + * of Java code, caching compiled classes, and managing file resources related to the compilation process. + * + *

This package is part of the OpenHFT project and is designed to facilitate seamless + * integration with other components, providing a flexible solution for dynamic code generation + * and execution.

+ */ +package net.openhft.compiler; diff --git a/src/test/java/eg/FooBarTee.java b/src/test/java/eg/FooBarTee.java index fc311ab..6bad4f7 100644 --- a/src/test/java/eg/FooBarTee.java +++ b/src/test/java/eg/FooBarTee.java @@ -49,6 +49,5 @@ public void stop() { public void close() { stop(); - } } diff --git a/src/test/java/mytest/RuntimeCompileTest.java b/src/test/java/mytest/RuntimeCompileTest.java index e1953da..4400cd5 100644 --- a/src/test/java/mytest/RuntimeCompileTest.java +++ b/src/test/java/mytest/RuntimeCompileTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.fail; public class RuntimeCompileTest { + // A string containing Java code to be compiled at runtime. The class implements `IntConsumer` and validates the value of a given integer. static String code = "package mytest;\n" + "public class Test implements IntConsumer {\n" + " public void accept(int num) {\n" + @@ -45,20 +46,23 @@ public class RuntimeCompileTest { " }\n" + "}\n"; + // Test to check if an `IllegalArgumentException` is thrown when a number out of byte range is passed. @Test public void outOfBounds() throws Exception { ClassLoader cl = new URLClassLoader(new URL[0]); Class aClass = CompilerUtils.CACHED_COMPILER. loadFromJava(cl, "mytest.Test", code); IntConsumer consumer = (IntConsumer) aClass.getDeclaredConstructor().newInstance(); - consumer.accept(1); // ok + consumer.accept(1); // ok, within byte range try { - consumer.accept(128); // no ok + consumer.accept(128); // not ok, out of byte range fail(); } catch (IllegalArgumentException expected) { + // Expected exception } } + // Test to validate multi-threaded compilation and execution. It compiles and runs a class implementing `IntConsumer` and `IntSupplier` with concurrent threads. //@Ignore("see https://teamcity.chronicle.software/viewLog.html?buildId=639347&tab=buildResultsDiv&buildTypeId=OpenHFT_BuildAll_BuildJava11compileJava11") @Test public void testMultiThread() throws Exception { @@ -69,8 +73,9 @@ public void testMultiThread() throws Exception { " public void accept(int num) {\n" + " called.incrementAndGet();\n" + " }\n"); - for (int j=0; j<1_000; j++) { - largeClass.append(" public void accept"+j+"(int num) {\n" + + // Building a large class with multiple `accept` methods + for (int j = 0; j < 1_000; j++) { + largeClass.append(" public void accept" + j + "(int num) {\n" + " if ((byte) num != num)\n" + " throw new IllegalArgumentException();\n" + " }\n"); @@ -85,7 +90,8 @@ public void testMultiThread() throws Exception { final AtomicInteger started = new AtomicInteger(0); final ExecutorService executor = Executors.newFixedThreadPool(nThreads); final List> futures = new ArrayList<>(); - for (int i=0; i { started.incrementAndGet(); @@ -101,10 +107,12 @@ public void testMultiThread() throws Exception { })); } executor.shutdown(); + // Waiting for all threads to complete for (Future f : futures) f.get(10, TimeUnit.SECONDS); Class aClass = cc.loadFromJava(cl, "mytest.Test2", code2); IntSupplier consumer = (IntSupplier) aClass.getDeclaredConstructor().newInstance(); + // Asserting that the consumer was called the same number of times as the number of threads assertEquals(nThreads, consumer.getAsInt()); } } diff --git a/src/test/java/net/openhft/compiler/CachedCompilerTest.java b/src/test/java/net/openhft/compiler/CachedCompilerTest.java new file mode 100644 index 0000000..175fc24 --- /dev/null +++ b/src/test/java/net/openhft/compiler/CachedCompilerTest.java @@ -0,0 +1,85 @@ +package net.openhft.compiler; + +import junit.framework.TestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class CachedCompilerTest extends TestCase { + private CachedCompiler cachedCompiler; + private StandardJavaFileManager standardJavaFileManager; + + @BeforeEach + public void setUp() { + cachedCompiler = new CachedCompiler(null, null); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + standardJavaFileManager = compiler.getStandardFileManager(null, null, null); + } + + @AfterEach + public void tearDown() throws IOException { + cachedCompiler.close(); + } + + @Test + public void testLoadFromJavaWithClassLoaderClassNameAndCode() { + String className = "net.openhft.compiler.TestClassB"; + String javaCode = "package net.openhft.compiler; public class TestClassB {}"; + ClassLoader customClassLoader = new ClassLoader(getClass().getClassLoader()) { + }; + assertDoesNotThrow(() -> cachedCompiler.loadFromJava(customClassLoader, className, javaCode)); + } + + @Test + public void testCompileFromJava() { + String className = "net.openhft.compiler.TestClass3"; + String javaCode = "package net.openhft.compiler; public class TestClass3 {}"; + MyJavaFileManager fileManager = new MyJavaFileManager(standardJavaFileManager); + Map result = cachedCompiler.compileFromJava(className, javaCode, fileManager); + assertNotNull(result); + assertFalse(result.isEmpty()); + assertTrue(result.containsKey(className)); + } + + @Test + public void testCompileFromJavaWithWriter() { + String className = "net.openhft.compiler.TestClass4"; + String javaCode = "package net.openhft.compiler; public class TestClass4 {}"; + PrintWriter writer = new PrintWriter(System.err); + MyJavaFileManager fileManager = new MyJavaFileManager(standardJavaFileManager); + Map result = cachedCompiler.compileFromJava(className, javaCode, writer, fileManager); + assertNotNull(result); + assertFalse(result.isEmpty()); + assertTrue(result.containsKey(className)); + } + + @Test + public void testLoadFromJavaWithWriter() { + String className = "net.openhft.compiler.TestClass5"; + String javaCode = "package net.openhft.compiler; public class TestClass5 {}"; + PrintWriter writer = new PrintWriter(System.err); + ClassLoader customClassLoader = new ClassLoader(getClass().getClassLoader()) { + }; + assertDoesNotThrow(() -> cachedCompiler.loadFromJava(customClassLoader, className, javaCode, writer)); + } + + @Test + public void testGetFileManager() { + MyJavaFileManager fileManager = cachedCompiler.getFileManager(standardJavaFileManager); + assertNotNull(fileManager); + } + + @Test + public void testClose() { + assertDoesNotThrow(() -> cachedCompiler.close()); + } +} \ No newline at end of file diff --git a/src/test/java/net/openhft/compiler/CloseableByteArrayOutputStreamTest.java b/src/test/java/net/openhft/compiler/CloseableByteArrayOutputStreamTest.java new file mode 100644 index 0000000..904b755 --- /dev/null +++ b/src/test/java/net/openhft/compiler/CloseableByteArrayOutputStreamTest.java @@ -0,0 +1,45 @@ +package net.openhft.compiler; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class CloseableByteArrayOutputStreamTest extends TestCase { + + private CloseableByteArrayOutputStream baos; + + @Override + protected void setUp() throws Exception { + super.setUp(); + baos = new CloseableByteArrayOutputStream(); + } + + public void testClose() { + assertFalse(baos.closeFuture().isDone()); + baos.close(); + assertTrue(baos.closeFuture().isDone()); + } + + public void testCloseFuture() throws ExecutionException, InterruptedException { + CompletableFuture future = baos.closeFuture(); + assertFalse(future.isDone()); + + baos.close(); + assertTrue(future.isDone()); + future.get(); // Ensure that future completes without exceptions + } + + public void testWrite() throws IOException { + String testString = "Hello, world!"; + baos.write(testString.getBytes()); + assertEquals(testString, baos.toString()); + } + + public void testSize() throws IOException { + String testString = "Hello, world!"; + baos.write(testString.getBytes()); + assertEquals(testString.length(), baos.size()); + } +} diff --git a/src/test/java/net/openhft/compiler/CompilerTest.java b/src/test/java/net/openhft/compiler/CompilerTest.java index 1828c05..1feb9c4 100644 --- a/src/test/java/net/openhft/compiler/CompilerTest.java +++ b/src/test/java/net/openhft/compiler/CompilerTest.java @@ -64,6 +64,7 @@ public void test_compiler() throws Throwable { new CachedCompiler(new File(parent, "target/generated-test-sources"), new File(parent, "target/test-classes")) : CompilerUtils.CACHED_COMPILER; + // Text to be used in the generated class, including the current date. String text = "generated test " + new Date(); try { final Class aClass = diff --git a/src/test/java/net/openhft/compiler/CompilerUtilsTest.java b/src/test/java/net/openhft/compiler/CompilerUtilsTest.java new file mode 100644 index 0000000..3d1da55 --- /dev/null +++ b/src/test/java/net/openhft/compiler/CompilerUtilsTest.java @@ -0,0 +1,89 @@ +package net.openhft.compiler; + +import junit.framework.TestCase; +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class CompilerUtilsTest extends TestCase { + + private File testFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + testFile = new File("testFile.txt"); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + if (testFile.exists()) { + assertTrue(testFile.delete()); + } + } + + @Test + public void testDecodeUTF8() { + byte[] bytes = "Hello, world!".getBytes(StandardCharsets.UTF_8); + String result = CompilerUtils.decodeUTF8(bytes); + assertEquals("Hello, world!", result); + } + + @Test + public void testClose() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write("test".getBytes(StandardCharsets.UTF_8)); + CompilerUtils.close(baos); + assertEquals("test", baos.toString(StandardCharsets.UTF_8.name())); + } + + @Test + public void testEncodeUTF8() { + String text = "Hello, world!"; + byte[] bytes = CompilerUtils.encodeUTF8(text); + assertNotNull(bytes); + assertEquals(text, new String(bytes, StandardCharsets.UTF_8)); + } + + @Test + public void testDefineClassWithClassNameAndBytes() { + String className = "net.openhft.compiler.TestDefineClassA"; + String javaCode = "package net.openhft.compiler; public class TestDefineClassA {}"; + byte[] byteCode = compileClass(className, javaCode); + assertNotNull(byteCode); + ClassLoader customClassLoader = new ClassLoader(getClass().getClassLoader()) { + }; + assertDoesNotThrow(() -> CompilerUtils.defineClass(customClassLoader, className, byteCode)); + } + + @Test + public void testDefineClassWithClassLoaderClassNameAndBytes() { + String className = "net.openhft.compiler.TestDefineClassB"; + String javaCode = "package net.openhft.compiler; public class TestDefineClassB {}"; + byte[] byteCode = compileClass(className, javaCode); + assertNotNull(byteCode); + ClassLoader customClassLoader = new ClassLoader(getClass().getClassLoader()) { + }; + assertDoesNotThrow(() -> CompilerUtils.defineClass(customClassLoader, className, byteCode)); + } + + private byte[] compileClass(String className, String javaCode) { + CachedCompiler cachedCompiler = new CachedCompiler(null, null); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager standardJavaFileManager = compiler.getStandardFileManager(null, null, null); + MyJavaFileManager fileManager = new MyJavaFileManager(standardJavaFileManager); + Map result = cachedCompiler.compileFromJava(className, javaCode, fileManager); + assertNotNull(result); + return result.get(className); + } +} diff --git a/src/test/java/net/openhft/compiler/JavaSourceFromStringTest.java b/src/test/java/net/openhft/compiler/JavaSourceFromStringTest.java new file mode 100644 index 0000000..f86794b --- /dev/null +++ b/src/test/java/net/openhft/compiler/JavaSourceFromStringTest.java @@ -0,0 +1,33 @@ +package net.openhft.compiler; + +import junit.framework.TestCase; + +import javax.tools.JavaFileObject; + +public class JavaSourceFromStringTest extends TestCase { + + private JavaSourceFromString javaSourceFromString; + + @Override + protected void setUp() throws Exception { + super.setUp(); + String name = "net.openhft.compiler.TestSource"; + String code = "package net.openhft.compiler; public class TestSource {}"; + javaSourceFromString = new JavaSourceFromString(name, code); + } + + public void testGetCharContent() { + boolean ignoreEncodingErrors = true; + CharSequence content = javaSourceFromString.getCharContent(ignoreEncodingErrors); + assertNotNull(content); + assertEquals("package net.openhft.compiler; public class TestSource {}", content.toString()); + } + + public void testGetKind() { + assertEquals(JavaFileObject.Kind.SOURCE, javaSourceFromString.getKind()); + } + + public void testToUri() { + assertEquals("string:///net/openhft/compiler/TestSource.java", javaSourceFromString.toUri().toString()); + } +} diff --git a/src/test/java/net/openhft/compiler/MyIntSupplier.java b/src/test/java/net/openhft/compiler/MyIntSupplier.java index 612d693..884ae8c 100644 --- a/src/test/java/net/openhft/compiler/MyIntSupplier.java +++ b/src/test/java/net/openhft/compiler/MyIntSupplier.java @@ -21,6 +21,16 @@ /* * Created by Peter Lawrey on 31/12/2016. */ +/** + * Interface representing a supplier of an integer value. + * Implementations of this interface provide a method to retrieve + * an integer value, possibly dynamically generated or computed. + */ public interface MyIntSupplier { + /** + * Retrieves an integer value. + * + * @return the supplied integer value + */ public int get(); }