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);
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);
}
}

View File

@@ -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";

View File

@@ -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.
*
* <p>Compilation output is written to {@code <contentRoot>/.oac-build}.
* Cache invalidation uses the content folder lastModified aggregate value.
*/
public final class JavaPageCache {
private final ConcurrentHashMap<String, Entry> cache = new ConcurrentHashMap<>();
public Class<?> getOrCompile(File javaFile) throws Exception {
/**
* 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();
long lm = javaFile.lastModified();
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) { }
}

View File

@@ -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.
*
* <p>Supports packages by deriving the fully qualified class name from source.
* <p>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<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);
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjectsFromFiles(sources);
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");
boolean success = compiler.getTask(null, fileManager, null, options, null, units).call();
fileManager.close();
if (!success) {
throw new RuntimeException("Compilation failed (see compiler output). Requested=" + requestedJavaFile.getName());
}
}
if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName());
String fqcn = deriveFqcn(requestedJavaFile);
String fqcn = deriveFqcn(javaFile);
URLClassLoader cl = new URLClassLoader(new URL[]{buildDir.toUri().toURL()}, JavaPageCompiler.class.getClassLoader());
Class<?> clazz = cl.loadClass(fqcn);
return new JavaPageCache.LoadedClass(cl, clazz);
}
// 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()});
return cl.loadClass(fqcn);
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 {
@@ -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;
}
}

View File

@@ -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.
*
* <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 {
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);

View File

@@ -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.
*
* <p>Behavior:
* <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>
* <p>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<String, RouteEntry> routes = 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) {
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);
// 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;
}
/**
* 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<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.
// 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
*/
public record RouteLookupResult(File sourceFile, String fqcn, boolean routable) {
}
public record RouteLookupResult(File sourceFile, String fqcn) { }
private record RouteMeta(String routePath, String fqcn) { }
}