diff --git a/src/main/java/org/openautonomousconnection/webserver/Main.java b/src/main/java/org/openautonomousconnection/webserver/Main.java index 0b3d52a..3bca1e1 100644 --- a/src/main/java/org/openautonomousconnection/webserver/Main.java +++ b/src/main/java/org/openautonomousconnection/webserver/Main.java @@ -96,12 +96,12 @@ public class Main { Scanner scanner = new Scanner(System.in); - while (protocolBridge.getProtocolServer().getNetwork().isServerOnline()) { - System.out.print(commandExecutor.getName() + "> "); - String line = scanner.nextLine(); - commandManager.execute(commandExecutor, line); - } - - System.exit(0); +// while (protocolBridge.getProtocolServer().getNetwork().isServerOnline()) { +// System.out.print(commandExecutor.getName() + "> "); +// String line = scanner.nextLine(); +// commandManager.execute(commandExecutor, line); +// } +// +// System.exit(0); } } \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/WebServer.java b/src/main/java/org/openautonomousconnection/webserver/WebServer.java index 2d49274..5457712 100644 --- a/src/main/java/org/openautonomousconnection/webserver/WebServer.java +++ b/src/main/java/org/openautonomousconnection/webserver/WebServer.java @@ -2,6 +2,7 @@ package org.openautonomousconnection.webserver; import dev.unlegitdqrk.unlegitlibrary.event.Listener; import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketReadEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketSendEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; import lombok.Getter; import org.openautonomousconnection.protocol.annotations.ProtocolInfo; @@ -64,7 +65,6 @@ public final class WebServer extends ProtocolWebServer { @Override public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { - try { String path = request.getPath() == null ? "/" : request.getPath(); @@ -94,6 +94,20 @@ public final class WebServer extends ProtocolWebServer { } } + // TODO: Temporary solution until its fixed in Protocol + @Listener + public void onRequest(S_PacketReadEvent event) { + if (event.getPacket() instanceof WebRequestPacket) { + try { + event.getClient().sendPacket( + onWebRequest(getClientByID(event.getClient().getUniqueID()), (WebRequestPacket) event.getPacket()), + TransportProtocol.TCP); + } catch (IOException e) { + getProtocolBridge().getLogger().exception("Failed to send web response", e); + } + } + } + private WebResponsePacket serveFile(CustomConnectedClient client, String path) throws Exception { if (path.startsWith("/")) path = path.substring(1); if (path.isEmpty()) path = "index.html"; diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java index 076cf8b..d43ed16 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCache.java @@ -1,29 +1,55 @@ package org.openautonomousconnection.webserver.runtime; import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** - * Caches compiled Java WebPages by file lastModified timestamp. + * Caches compiled Java page classes for the content tree. + * + *

Compilation output is written to {@code /.oac-build}. + * Cache invalidation uses the content folder lastModified aggregate value. */ public final class JavaPageCache { private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - public Class getOrCompile(File javaFile) throws Exception { - String key = javaFile.getAbsolutePath(); - long lm = javaFile.lastModified(); + /** + * Compiles the content tree if needed and returns a loaded class using a build-dir ClassLoader. + * + * @param contentRoot content root directory + * @param javaFile requested Java source file + * @param contentLastModified aggregate last modified of the whole content tree + * @return loaded class + * @throws Exception on compilation or classloading errors + */ + public LoadedClass getOrCompile(File contentRoot, File javaFile, long contentLastModified) throws Exception { + Objects.requireNonNull(contentRoot, "contentRoot"); + Objects.requireNonNull(javaFile, "javaFile"); + String key = javaFile.getAbsolutePath(); Entry e = cache.get(key); - if (e != null && e.lastModified == lm) { - return e.clazz; + + if (e != null && e.contentLastModified == contentLastModified && e.loadedClass != null) { + return e.loadedClass; } - Class compiled = JavaPageCompiler.compile(javaFile); - cache.put(key, new Entry(lm, compiled)); - return compiled; + LoadedClass loaded = JavaPageCompiler.compileAllAndLoad(contentRoot, javaFile); + cache.put(key, new Entry(contentLastModified, loaded)); + return loaded; } - private record Entry(long lastModified, Class clazz) { - } -} + private record Entry(long contentLastModified, LoadedClass loadedClass) { } + + /** + * Result wrapper holding both the ClassLoader and the loaded class. + * + * @param classLoader loader used to load the class + * @param clazz loaded class + */ + public record LoadedClass(ClassLoader classLoader, Class clazz) { } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java index bc8f570..284d0c9 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageCompiler.java @@ -9,39 +9,70 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; /** - * Compiles and loads Java pages at runtime. - * - *

Supports packages by deriving the fully qualified class name from source. - *

NOTE: Requires a JDK (ToolProvider.getSystemJavaCompiler != null). + * Runtime compiler for Java pages located under the server content root. */ public final class JavaPageCompiler { private JavaPageCompiler() { } - public static Class compile(File javaFile) throws Exception { + /** + * Compiles all Java sources under {@code contentRoot} into {@code .oac-build} + * and loads the class corresponding to {@code requestedJavaFile}. + * + * @param contentRoot content root + * @param requestedJavaFile requested .java file + * @return loaded class with its loader + * @throws Exception on compilation/load error + */ + public static JavaPageCache.LoadedClass compileAllAndLoad(File contentRoot, File requestedJavaFile) throws Exception { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) throw new IllegalStateException("JDK required (JavaCompiler not available)"); - StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); - Iterable units = fileManager.getJavaFileObjects(javaFile); + Path buildDir = contentRoot.toPath().resolve(".oac-build"); + Files.createDirectories(buildDir); - List options = List.of("-classpath", System.getProperty("java.class.path")); + List sources = listJavaFiles(contentRoot); + if (sources.isEmpty()) throw new IllegalStateException("No .java files found under content root: " + contentRoot); - boolean success = compiler.getTask(null, fileManager, null, options, null, units).call(); - fileManager.close(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + Iterable units = fileManager.getJavaFileObjectsFromFiles(sources); - if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName()); + List options = new ArrayList<>(); + options.add("-classpath"); + options.add(System.getProperty("java.class.path")); + options.add("-d"); + options.add(buildDir.toAbsolutePath().toString()); + options.add("-encoding"); + options.add("UTF-8"); - String fqcn = deriveFqcn(javaFile); + boolean success = compiler.getTask(null, fileManager, null, options, null, units).call(); + if (!success) { + throw new RuntimeException("Compilation failed (see compiler output). Requested=" + requestedJavaFile.getName()); + } + } - // Root must be the folder containing the package root (javaFile parent is fine if classes output there). - URLClassLoader cl = new URLClassLoader(new URL[]{javaFile.getParentFile().toURI().toURL()}); + String fqcn = deriveFqcn(requestedJavaFile); - return cl.loadClass(fqcn); + URLClassLoader cl = new URLClassLoader(new URL[]{buildDir.toUri().toURL()}, JavaPageCompiler.class.getClassLoader()); + Class clazz = cl.loadClass(fqcn); + return new JavaPageCache.LoadedClass(cl, clazz); + } + + private static List listJavaFiles(File root) { + List out = new ArrayList<>(); + File[] children = root.listFiles(); + if (children == null) return out; + for (File c : children) { + if (c.isDirectory()) out.addAll(listJavaFiles(c)); + else if (c.isFile() && c.getName().endsWith(".java")) out.add(c); + } + return out; } private static String deriveFqcn(File javaFile) throws Exception { @@ -54,8 +85,9 @@ public final class JavaPageCompiler { pkg = t.substring("package ".length(), t.length() - 1).trim(); break; } - // Stop scanning early if class appears before package (invalid anyway) - if (t.startsWith("public class") || t.startsWith("class ") || t.startsWith("public final class")) { + if (t.startsWith("public class") || t.startsWith("class ") + || t.startsWith("public final class") || t.startsWith("public interface") + || t.startsWith("interface ") || t.startsWith("public enum") || t.startsWith("enum ")) { break; } } @@ -64,4 +96,4 @@ public final class JavaPageCompiler { if (pkg == null || pkg.isBlank()) return simple; return pkg + "." + simple; } -} +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java index 34b58ed..8dc406e 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaPageDispatcher.java @@ -16,59 +16,68 @@ import java.nio.charset.StandardCharsets; import java.util.Map; /** - * Dispatches Java WebPages using @Route annotation. + * Dispatches Java WebPages using {@code @Route} annotation. + * + *

This dispatcher relies on {@link JavaRouteRegistry} for route-to-source mapping + * and uses {@link JavaPageCache} to compile/load classes from the content tree. */ public final class JavaPageDispatcher { private static final JavaRouteRegistry ROUTES = new JavaRouteRegistry(); + private static final JavaPageCache CACHE = new JavaPageCache(); private JavaPageDispatcher() { } + /** + * Attempts to dispatch a request to a Java WebPage. + * + * @param client connected client + * @param server protocol web server + * @param request request packet + * @return response packet or {@code null} if no Java route matches and static file handling should proceed + * @throws Exception on unexpected failures + */ public static WebResponsePacket dispatch( CustomConnectedClient client, ProtocolWebServer server, WebRequestPacket request ) throws Exception { - if (request.getPath() == null) return null; + if (request == null || request.getPath() == null) { + return null; + } String route = request.getPath(); - if (!route.startsWith("/")) route = "/" + route; + if (!route.startsWith("/")) { + route = "/" + route; + } File contentRoot = server.getContentFolder(); ROUTES.refreshIfNeeded(contentRoot); JavaRouteRegistry.RouteLookupResult found = ROUTES.find(route); - - // If no @Route match, still detect "requested a .java-backed path by filename" - // (legacy behavior): if a matching *.java exists, compile/load but return error. if (found == null) { - String p = route.startsWith("/") ? route.substring(1) : route; - File javaFile = new File(contentRoot, p + ".java"); - if (javaFile.exists() && javaFile.isFile()) { - // Compile/load but do not serve. - new JavaPageCache().getOrCompile(javaFile); - return error(501, "Java class exists but has no @Route: " + route); - } - return null; // not a Java route -> let file server handle it + return null; } - // If it has @Route but is not a WebPage, compile/load but return error. - if (!found.routable()) { + long contentLm = ROUTES.currentContentLastModified(contentRoot); + + JavaPageCache.LoadedClass loaded = CACHE.getOrCompile(contentRoot, found.sourceFile(), contentLm); + Class clazz = loaded.clazz(); + + // Verify that the loaded class is actually routable. + if (!WebPage.class.isAssignableFrom(clazz)) { return error(500, "Class has @Route but is not a WebPage: " + found.fqcn()); } - // Load and execute - Class clazz = Class.forName(found.fqcn()); Object instance = clazz.getDeclaredConstructor().newInstance(); - - if (!(instance instanceof WebPage page)) { - return error(500, "Routed class is not a WebPage: " + found.fqcn()); - } + WebPage page = (WebPage) instance; WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null; - if (hasher == null) return error(500, "WebHasher missing on server instance."); + if (hasher == null) { + return error(500, "WebHasher missing on server instance."); + } WebPageContext ctx = new WebPageContext(client, server, request, new RequestParams(request), hasher); return page.handle(ctx); @@ -82,4 +91,4 @@ public final class JavaPageDispatcher { msg.getBytes(StandardCharsets.UTF_8) ); } -} +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaRouteRegistry.java b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaRouteRegistry.java index ecdbae2..13179d6 100644 --- a/src/main/java/org/openautonomousconnection/webserver/runtime/JavaRouteRegistry.java +++ b/src/main/java/org/openautonomousconnection/webserver/runtime/JavaRouteRegistry.java @@ -1,30 +1,88 @@ package org.openautonomousconnection.webserver.runtime; import org.openautonomousconnection.webserver.api.Route; -import org.openautonomousconnection.webserver.api.WebPage; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * Scans the content folder for .java files, compiles them and maps @Route paths to classes/files. + * Scans the content folder for .java files and maps {@code @Route(path="...")} to files/FQCN. * - *

Behavior: - *

+ *

This registry does NOT compile classes. Compilation is handled by the dispatcher/compiler cache. */ public final class JavaRouteRegistry { - private final JavaPageCache cache = new JavaPageCache(); private final ConcurrentHashMap routes = new ConcurrentHashMap<>(); private final ConcurrentHashMap scanState = new ConcurrentHashMap<>(); + private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([a-zA-Z0-9_\\.]+)\\s*;\\s*$"); + private static final Pattern ROUTE_PATTERN = Pattern.compile("@Route\\s*\\(\\s*path\\s*=\\s*\"([^\"]+)\"\\s*\\)"); + + /** + * Returns the aggregate last modified timestamp of the content tree. + * + * @param contentRoot content root + * @return last modified max value + */ + public long currentContentLastModified(File contentRoot) { + return folderLastModified(contentRoot); + } + + /** + * Refreshes registry when content folder changed. + * + * @param contentRoot content root + */ + public void refreshIfNeeded(File contentRoot) { + if (contentRoot == null) return; + + long lm = folderLastModified(contentRoot); + Long prev = scanState.get("lm"); + + if (prev != null && prev == lm && !routes.isEmpty()) { + return; + } + + scanState.put("lm", lm); + rebuild(contentRoot); + } + + /** + * Finds a route mapping. + * + * @param route route path + * @return result or null + */ + public RouteLookupResult find(String route) { + RouteEntry e = routes.get(route); + if (e == null) return null; + return new RouteLookupResult(e.sourceFile, e.fqcn); + } + + private void rebuild(File contentRoot) { + routes.clear(); + + for (File f : listJavaFiles(contentRoot)) { + try { + RouteMeta meta = parseRouteMeta(f); + if (meta == null) continue; + + String route = normalizeRoute(meta.routePath()); + routes.put(route, new RouteEntry(f, meta.fqcn())); + } catch (Exception ignored) { + // Ignore invalid sources; compilation will surface errors later when requested. + } + } + } + private static String normalizeRoute(String value) { if (value == null) return "/"; String v = value.trim(); @@ -44,11 +102,8 @@ public final class JavaRouteRegistry { if (children == null) continue; for (File c : children) { - if (c.isDirectory()) { - stack.push(c); - } else if (c.isFile() && c.getName().endsWith(".java")) { - out.add(c); - } + if (c.isDirectory()) stack.push(c); + else if (c.isFile() && c.getName().endsWith(".java")) out.add(c); } } @@ -65,68 +120,46 @@ public final class JavaRouteRegistry { return lm; } - /** - * Refreshes registry when content folder changed. - * - * @param contentRoot content root - */ - public void refreshIfNeeded(File contentRoot) { - if (contentRoot == null) return; - long lm = folderLastModified(contentRoot); + private static RouteMeta parseRouteMeta(File javaFile) throws Exception { + String src = Files.readString(javaFile.toPath(), StandardCharsets.UTF_8); - Long prev = scanState.get("lm"); - if (prev != null && prev == lm && !routes.isEmpty()) return; + // Route annotation + Matcher rm = ROUTE_PATTERN.matcher(src); + if (!rm.find()) return null; + String routePath = rm.group(1); - scanState.put("lm", lm); - rebuild(contentRoot); - } - - /** - * Finds a routable class for a route. - * - * @param route route path - * @return entry or null - */ - public RouteLookupResult find(String route) { - RouteEntry e = routes.get(route); - if (e == null) return null; - return new RouteLookupResult(e.sourceFile, e.fqcn, e.routable); - } - - private void rebuild(File contentRoot) { - routes.clear(); - - List javaFiles = listJavaFiles(contentRoot); - for (File f : javaFiles) { - try { - long lm = f.lastModified(); - Class clazz = cache.getOrCompile(f); - - Route r = clazz.getAnnotation(Route.class); - boolean isWebPage = WebPage.class.isAssignableFrom(clazz); - boolean routable = (r != null && isWebPage); - - if (r != null) { - String path = normalizeRoute(r.path()); - routes.put(path, new RouteEntry(f, lm, clazz.getName(), routable)); - } - - } catch (Exception ignored) { - // Compilation errors are handled later when requested or during next refresh. + // Package (optional) + String pkg = null; + for (String line : src.split("\\R")) { + Matcher pm = PACKAGE_PATTERN.matcher(line); + if (pm.matches()) { + pkg = pm.group(1); + break; + } + // Early stop if class starts before a package declaration + String t = line.trim(); + if (t.startsWith("public class") || t.startsWith("class ") + || t.startsWith("public final class") || t.startsWith("public interface") + || t.startsWith("interface ") || t.startsWith("public enum") || t.startsWith("enum ")) { + break; } } + + String simple = javaFile.getName().replace(".java", ""); + String fqcn = (pkg == null || pkg.isBlank()) ? simple : (pkg + "." + simple); + + return new RouteMeta(routePath, fqcn); } - private record RouteEntry(File sourceFile, long lastModified, String fqcn, boolean routable) { - } + private record RouteEntry(File sourceFile, String fqcn) { } /** * Lookup result for a route. * * @param sourceFile Java source file - * @param fqcn fully qualified class name - * @param routable true if @Route + implements WebPage + * @param fqcn fully qualified class name */ - public record RouteLookupResult(File sourceFile, String fqcn, boolean routable) { - } -} + public record RouteLookupResult(File sourceFile, String fqcn) { } + + private record RouteMeta(String routePath, String fqcn) { } +} \ No newline at end of file