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 extends JavaFileObject> 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 extends JavaFileObject> 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:
- *
- * - If class has @Route AND implements WebPage => routable
- * - If class is compiled but not routable => still cached as loaded, but never served
- *
+ * 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