Changed Web Routing and added WebHash
This commit is contained in:
@@ -4,12 +4,15 @@ import javax.tools.*;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Compiles and loads Java web pages at runtime.
|
||||
* Compiles and loads Java pages at runtime.
|
||||
*
|
||||
* NOTE: Requires running with a JDK (ToolProvider.getSystemJavaCompiler != null).
|
||||
* <p>Supports packages by deriving the fully qualified class name from source.
|
||||
* <p>NOTE: Requires a JDK (ToolProvider.getSystemJavaCompiler != null).
|
||||
*/
|
||||
public final class JavaPageCompiler {
|
||||
|
||||
@@ -20,10 +23,8 @@ public final class JavaPageCompiler {
|
||||
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);
|
||||
|
||||
// Compile in-place (class next to the .java file)
|
||||
List<String> options = List.of("-classpath", System.getProperty("java.class.path"));
|
||||
|
||||
boolean success = compiler.getTask(null, fileManager, null, options, null, units).call();
|
||||
@@ -31,9 +32,32 @@ public final class JavaPageCompiler {
|
||||
|
||||
if (!success) throw new RuntimeException("Compilation failed: " + javaFile.getName());
|
||||
|
||||
String fqcn = deriveFqcn(javaFile);
|
||||
|
||||
// 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 className = javaFile.getName().replace(".java", "");
|
||||
return cl.loadClass(className);
|
||||
return cl.loadClass(fqcn);
|
||||
}
|
||||
|
||||
private static String deriveFqcn(File javaFile) throws Exception {
|
||||
String src = Files.readString(javaFile.toPath(), StandardCharsets.UTF_8);
|
||||
|
||||
String pkg = null;
|
||||
for (String line : src.split("\\R")) {
|
||||
String t = line.trim();
|
||||
if (t.startsWith("package ") && t.endsWith(";")) {
|
||||
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")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String simple = javaFile.getName().replace(".java", "");
|
||||
if (pkg == null || pkg.isBlank()) return simple;
|
||||
return pkg + "." + simple;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,22 @@ import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestP
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
|
||||
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
|
||||
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||
import org.openautonomousconnection.webserver.WebServer;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||
import org.openautonomousconnection.webserver.utils.WebHasher;
|
||||
import org.openautonomousconnection.webserver.utils.RequestParams;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Dispatches Java WebPages using @Route annotation.
|
||||
*/
|
||||
public final class JavaPageDispatcher {
|
||||
|
||||
private static final JavaPageCache CACHE = new JavaPageCache();
|
||||
private static final JavaRouteRegistry ROUTES = new JavaRouteRegistry();
|
||||
|
||||
private JavaPageDispatcher() {}
|
||||
|
||||
@@ -23,18 +31,53 @@ public final class JavaPageDispatcher {
|
||||
|
||||
if (request.getPath() == null) return null;
|
||||
|
||||
String p = request.getPath().startsWith("/") ? request.getPath().substring(1) : request.getPath();
|
||||
File javaFile = new File(server.getContentFolder(), p + ".java");
|
||||
String route = request.getPath();
|
||||
if (!route.startsWith("/")) route = "/" + route;
|
||||
|
||||
if (!javaFile.exists() || !javaFile.isFile()) return null;
|
||||
File contentRoot = server.getContentFolder();
|
||||
ROUTES.refreshIfNeeded(contentRoot);
|
||||
|
||||
Class<?> clazz = CACHE.getOrCompile(javaFile);
|
||||
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
|
||||
}
|
||||
|
||||
// If it has @Route but is not a WebPage, compile/load but return error.
|
||||
if (!found.routable()) {
|
||||
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))
|
||||
throw new IllegalStateException("Java page must implement WebPage");
|
||||
if (!(instance instanceof WebPage page)) {
|
||||
return error(500, "Routed class is not a WebPage: " + found.fqcn());
|
||||
}
|
||||
|
||||
WebPageContext ctx = new WebPageContext(client, server, request);
|
||||
WebHasher hasher = (server instanceof WebServer ws) ? ws.getHasher() : null;
|
||||
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);
|
||||
}
|
||||
|
||||
private static WebResponsePacket error(int code, String msg) {
|
||||
return new WebResponsePacket(
|
||||
code,
|
||||
"text/plain; charset=utf-8",
|
||||
Map.of(),
|
||||
msg.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.openautonomousconnection.webserver.runtime;
|
||||
|
||||
import org.openautonomousconnection.webserver.api.Route;
|
||||
import org.openautonomousconnection.webserver.api.WebPage;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Scans the content folder for .java files, compiles them and maps @Route paths to classes/files.
|
||||
*
|
||||
* <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>
|
||||
*/
|
||||
public final class JavaRouteRegistry {
|
||||
|
||||
private static final class RouteEntry {
|
||||
final File sourceFile;
|
||||
final long lastModified;
|
||||
final String fqcn;
|
||||
final boolean routable;
|
||||
|
||||
RouteEntry(File sourceFile, long lastModified, String fqcn, boolean routable) {
|
||||
this.sourceFile = sourceFile;
|
||||
this.lastModified = lastModified;
|
||||
this.fqcn = fqcn;
|
||||
this.routable = routable;
|
||||
}
|
||||
}
|
||||
|
||||
private final JavaPageCache cache = new JavaPageCache();
|
||||
private final ConcurrentHashMap<String, RouteEntry> routes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> scanState = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeRoute(String value) {
|
||||
if (value == null) return "/";
|
||||
String v = value.trim();
|
||||
if (v.isEmpty()) return "/";
|
||||
if (!v.startsWith("/")) v = "/" + v;
|
||||
return v;
|
||||
}
|
||||
|
||||
private static List<File> listJavaFiles(File root) {
|
||||
ArrayList<File> out = new ArrayList<>();
|
||||
Deque<File> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
File cur = stack.pop();
|
||||
File[] children = cur.listFiles();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static long folderLastModified(File folder) {
|
||||
long lm = folder.lastModified();
|
||||
File[] children = folder.listFiles();
|
||||
if (children == null) return lm;
|
||||
for (File c : children) {
|
||||
lm = Math.max(lm, c.isDirectory() ? folderLastModified(c) : c.lastModified());
|
||||
}
|
||||
return lm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user