Bug fixes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
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) { }
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
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<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).
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
// 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) { }
|
||||
}
|
||||
Reference in New Issue
Block a user