Bug fixes

This commit is contained in:
UnlegitDqrk
2026-02-08 22:33:31 +01:00
parent e9c06f571f
commit fe11dd7701
6 changed files with 242 additions and 128 deletions

View File

@@ -96,12 +96,12 @@ public class Main {
Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
while (protocolBridge.getProtocolServer().getNetwork().isServerOnline()) { // while (protocolBridge.getProtocolServer().getNetwork().isServerOnline()) {
System.out.print(commandExecutor.getName() + "> "); // System.out.print(commandExecutor.getName() + "> ");
String line = scanner.nextLine(); // String line = scanner.nextLine();
commandManager.execute(commandExecutor, line); // commandManager.execute(commandExecutor, line);
} // }
//
System.exit(0); // System.exit(0);
} }
} }

View File

@@ -2,6 +2,7 @@ package org.openautonomousconnection.webserver;
import dev.unlegitdqrk.unlegitlibrary.event.Listener; 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_PacketReadEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.S_PacketSendEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
import lombok.Getter; import lombok.Getter;
import org.openautonomousconnection.protocol.annotations.ProtocolInfo; import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
@@ -64,7 +65,6 @@ public final class WebServer extends ProtocolWebServer {
@Override @Override
public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) { public WebResponsePacket onWebRequest(CustomConnectedClient client, WebRequestPacket request) {
try { try {
String path = request.getPath() == null ? "/" : request.getPath(); 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 { private WebResponsePacket serveFile(CustomConnectedClient client, String path) throws Exception {
if (path.startsWith("/")) path = path.substring(1); if (path.startsWith("/")) path = path.substring(1);
if (path.isEmpty()) path = "index.html"; if (path.isEmpty()) path = "index.html";

View File

@@ -1,29 +1,55 @@
package org.openautonomousconnection.webserver.runtime; package org.openautonomousconnection.webserver.runtime;
import java.io.File; 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; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Caches compiled Java WebPages by file lastModified timestamp. * Caches compiled Java page classes for the content tree.
*
* <p>Compilation output is written to {@code <contentRoot>/.oac-build}.
* Cache invalidation uses the content folder lastModified aggregate value.
*/ */
public final class JavaPageCache { public final class JavaPageCache {
private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>();
public Class<?> getOrCompile(File javaFile) throws Exception { /**
String key = javaFile.getAbsolutePath(); * Compiles the content tree if needed and returns a loaded class using a build-dir ClassLoader.
long lm = javaFile.lastModified(); *
* @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); 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); LoadedClass loaded = JavaPageCompiler.compileAllAndLoad(contentRoot, javaFile);
cache.put(key, new Entry(lm, compiled)); cache.put(key, new Entry(contentLastModified, loaded));
return compiled; 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) { }
} }

View File

@@ -9,39 +9,70 @@ import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Compiles and loads Java pages at runtime. * Runtime compiler for Java pages located under the server content root.
*
* <p>Supports packages by deriving the fully qualified class name from source.
* <p>NOTE: Requires a JDK (ToolProvider.getSystemJavaCompiler != null).
*/ */
public final class JavaPageCompiler { public final class JavaPageCompiler {
private 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(); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) throw new IllegalStateException("JDK required (JavaCompiler not available)"); if (compiler == null) throw new IllegalStateException("JDK required (JavaCompiler not available)");
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); Path buildDir = contentRoot.toPath().resolve(".oac-build");
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(javaFile); Files.createDirectories(buildDir);
List<String> options = List.of("-classpath", System.getProperty("java.class.path")); List<File> 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(); try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
fileManager.close(); Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjectsFromFiles(sources);
if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName()); List<String> 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). String fqcn = deriveFqcn(requestedJavaFile);
URLClassLoader cl = new URLClassLoader(new URL[]{javaFile.getParentFile().toURI().toURL()});
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<File> listJavaFiles(File root) {
List<File> 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 { 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(); pkg = t.substring("package ".length(), t.length() - 1).trim();
break; break;
} }
// Stop scanning early if class appears before package (invalid anyway) if (t.startsWith("public class") || t.startsWith("class ")
if (t.startsWith("public class") || t.startsWith("class ") || t.startsWith("public final class")) { || t.startsWith("public final class") || t.startsWith("public interface")
|| t.startsWith("interface ") || t.startsWith("public enum") || t.startsWith("enum ")) {
break; break;
} }
} }

View File

@@ -16,59 +16,68 @@ import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
/** /**
* Dispatches Java WebPages using @Route annotation. * Dispatches Java WebPages using {@code @Route} annotation.
*
* <p>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 { public final class JavaPageDispatcher {
private static final JavaRouteRegistry ROUTES = new JavaRouteRegistry(); private static final JavaRouteRegistry ROUTES = new JavaRouteRegistry();
private static final JavaPageCache CACHE = new JavaPageCache();
private JavaPageDispatcher() { 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( public static WebResponsePacket dispatch(
CustomConnectedClient client, CustomConnectedClient client,
ProtocolWebServer server, ProtocolWebServer server,
WebRequestPacket request WebRequestPacket request
) throws Exception { ) throws Exception {
if (request.getPath() == null) return null; if (request == null || request.getPath() == null) {
return null;
}
String route = request.getPath(); String route = request.getPath();
if (!route.startsWith("/")) route = "/" + route; if (!route.startsWith("/")) {
route = "/" + route;
}
File contentRoot = server.getContentFolder(); File contentRoot = server.getContentFolder();
ROUTES.refreshIfNeeded(contentRoot); ROUTES.refreshIfNeeded(contentRoot);
JavaRouteRegistry.RouteLookupResult found = ROUTES.find(route); 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) { if (found == null) {
String p = route.startsWith("/") ? route.substring(1) : route; return null;
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
} }
// If it has @Route but is not a WebPage, compile/load but return error. long contentLm = ROUTES.currentContentLastModified(contentRoot);
if (!found.routable()) {
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()); 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(); Object instance = clazz.getDeclaredConstructor().newInstance();
WebPage page = (WebPage) instance;
if (!(instance instanceof WebPage page)) {
return error(500, "Routed class is not a WebPage: " + found.fqcn());
}
WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null; 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); WebPageContext ctx = new WebPageContext(client, server, request, new RequestParams(request), hasher);
return page.handle(ctx); return page.handle(ctx);

View File

@@ -1,30 +1,88 @@
package org.openautonomousconnection.webserver.runtime; package org.openautonomousconnection.webserver.runtime;
import org.openautonomousconnection.webserver.api.Route; import org.openautonomousconnection.webserver.api.Route;
import org.openautonomousconnection.webserver.api.WebPage;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; 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.
* *
* <p>Behavior: * <p>This registry does NOT compile classes. Compilation is handled by the dispatcher/compiler cache.
* <ul>
* <li>If class has @Route AND implements WebPage => routable</li>
* <li>If class is compiled but not routable => still cached as loaded, but never served</li>
* </ul>
*/ */
public final class JavaRouteRegistry { public final class JavaRouteRegistry {
private final JavaPageCache cache = new JavaPageCache();
private final ConcurrentHashMap<String, RouteEntry> routes = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, RouteEntry> routes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Long> scanState = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Long> 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) { private static String normalizeRoute(String value) {
if (value == null) return "/"; if (value == null) return "/";
String v = value.trim(); String v = value.trim();
@@ -44,11 +102,8 @@ public final class JavaRouteRegistry {
if (children == null) continue; if (children == null) continue;
for (File c : children) { for (File c : children) {
if (c.isDirectory()) { if (c.isDirectory()) stack.push(c);
stack.push(c); else if (c.isFile() && c.getName().endsWith(".java")) out.add(c);
} else if (c.isFile() && c.getName().endsWith(".java")) {
out.add(c);
}
} }
} }
@@ -65,68 +120,46 @@ public final class JavaRouteRegistry {
return lm; return lm;
} }
/** private static RouteMeta parseRouteMeta(File javaFile) throws Exception {
* Refreshes registry when content folder changed. String src = Files.readString(javaFile.toPath(), StandardCharsets.UTF_8);
*
* @param contentRoot content root
*/
public void refreshIfNeeded(File contentRoot) {
if (contentRoot == null) return;
long lm = folderLastModified(contentRoot);
Long prev = scanState.get("lm"); // Route annotation
if (prev != null && prev == lm && !routes.isEmpty()) return; Matcher rm = ROUTE_PATTERN.matcher(src);
if (!rm.find()) return null;
String routePath = rm.group(1);
scanState.put("lm", lm); // Package (optional)
rebuild(contentRoot); String pkg = null;
} for (String line : src.split("\\R")) {
Matcher pm = PACKAGE_PATTERN.matcher(line);
/** if (pm.matches()) {
* Finds a routable class for a route. pkg = pm.group(1);
* break;
* @param route route path }
* @return entry or null // Early stop if class starts before a package declaration
*/ String t = line.trim();
public RouteLookupResult find(String route) { if (t.startsWith("public class") || t.startsWith("class ")
RouteEntry e = routes.get(route); || t.startsWith("public final class") || t.startsWith("public interface")
if (e == null) return null; || t.startsWith("interface ") || t.startsWith("public enum") || t.startsWith("enum ")) {
return new RouteLookupResult(e.sourceFile, e.fqcn, e.routable); break;
}
private void rebuild(File contentRoot) {
routes.clear();
List<File> 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.
} }
} }
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. * Lookup result for a route.
* *
* @param sourceFile Java source file * @param sourceFile Java source file
* @param fqcn fully qualified class name * @param fqcn fully qualified class name
* @param routable true if @Route + implements WebPage
*/ */
public record RouteLookupResult(File sourceFile, String fqcn, boolean routable) { public record RouteLookupResult(File sourceFile, String fqcn) { }
}
private record RouteMeta(String routePath, String fqcn) { }
} }