diff --git a/README.md b/README.md
index 2f0bc90..48e8d85 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,24 @@
# InfoName - Lib
-InfoName and URL library to make client-side connection set-ups easier
+InfoName and URL library to make client-side connection set-ups easier
Just set the protocol schemes to OAC-InfoName ones by calling:
-InfoNames.registerOACInfoNameProtocols()
\ No newline at end of file
+
+```java
+import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
+import org.openautonomousconnection.infonamelib.OacWebUrlInstaller;
+import org.openautonomousconnection.infonamelib.ProtocolHandlerPackages;
+import org.openautonomousconnection.protocol.side.client.ProtocolClient;
+
+class Example {
+ private void init() {
+ EventManager eventManager = new EventManager();
+ ProtocolClient client = new MyImpl();
+
+ ProtocolHandlerPackages.installPackage("org.openautonomousconnection.infonamelib");
+ OacWebUrlInstaller.installOnce(eventManager, client);
+ }
+}
+```
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8d8dda3..d517320 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,12 +1,12 @@
-
4.0.0
org.openautonomousconnection
InfoNameLib
- 1.0.0-BETA.1.1
+ 1.0.0-BETA.1.2
Open Autonomous Connection
https://open-autonomous-connection.org/
@@ -94,6 +94,11 @@
6.0.0
test
+
+ org.openautonomousconnection
+ Protocol
+ 1.0.0-BETA.7.7
+
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java b/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java
deleted file mode 100644
index 566df08..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/InfoNameURLStreamHandlerFactory.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib;
-
-import org.openautonomousconnection.infonamelib.protocols.ftp.FTPInfoNameURLStreamHandler;
-import org.openautonomousconnection.infonamelib.protocols.web.WebInfoNameURLStreamHandler;
-
-import java.net.URLStreamHandler;
-import java.net.URLStreamHandlerFactory;
-
-public class InfoNameURLStreamHandlerFactory implements URLStreamHandlerFactory {
- @Override
- public URLStreamHandler createURLStreamHandler(String protocol) {
- return switch (protocol) {
- case "web" -> new WebInfoNameURLStreamHandler();
- case "ftp" -> new FTPInfoNameURLStreamHandler();
- default -> null;
- };
-
-
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java b/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java
deleted file mode 100644
index 04ba44c..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/InfoNames.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib;
-
-import java.net.URL;
-
-public class InfoNames {
- /**
- * Switches accepted Schemes in URLs to InfoName ones
- */
- public static void registerOACInfoNameProtocols() {
- URL.setURLStreamHandlerFactory(new InfoNameURLStreamHandlerFactory());
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java
new file mode 100644
index 0000000..0b73e1f
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebPacketListener.java
@@ -0,0 +1,173 @@
+package org.openautonomousconnection.infonamelib;
+
+import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
+import dev.unlegitdqrk.unlegitlibrary.event.EventPriority;
+import dev.unlegitdqrk.unlegitlibrary.event.Listener;
+import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSResponsePacket;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamChunkPacket;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamEndPacket;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.stream.WebStreamStartPacket;
+import org.openautonomousconnection.protocol.side.client.ProtocolClient;
+import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent;
+import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
+import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Receives incoming OAC web response packets and forwards them into the request broker.
+ *
+ * Important:
+ *
+ * - The shown protocol types do not contain any correlation id.
+ * - Therefore, the broker must treat the connection as single-flight (one in-flight request).
+ *
+ */
+public final class OacWebPacketListener extends EventListener {
+
+ private final OacWebRequestBroker broker;
+ private final ProtocolClient client;
+
+ /**
+ * Creates a new listener bound to the given broker.
+ *
+ * @param broker broker instance
+ */
+ public OacWebPacketListener(OacWebRequestBroker broker, ProtocolClient client) {
+ this.broker = Objects.requireNonNull(broker, "broker");
+ this.client = Objects.requireNonNull(client, "client");
+ }
+
+ @Listener(priority = EventPriority.HIGHEST)
+ public void onConnected(ConnectedToProtocolServerEvent event) {
+ OacWebRequestBroker.get().notifyServerConnected();
+ }
+
+ @Listener(priority = EventPriority.HIGHEST)
+ public void onPacketRead(C_PacketReadEvent event) {
+ Object p = event.getPacket();
+
+ if (p instanceof INSResponsePacket resp) {
+ INSResponseStatus status = resp.getStatus();
+ List records = resp.getRecords();
+
+ if (status != INSResponseStatus.OK) {
+ broker.invalidateCurrentInfoName();
+ throw new IllegalStateException("INS resolution failed: " + status);
+ }
+
+ if (records == null || records.isEmpty() || records.getFirst() == null || records.getFirst().value == null) {
+ broker.invalidateCurrentInfoName();
+ throw new IllegalStateException("INS resolution returned no usable records");
+ }
+
+ String host = records.getFirst().value.trim();
+ if (host.isEmpty()) {
+ broker.invalidateCurrentInfoName();
+ throw new IllegalStateException("INS record value is empty");
+ }
+
+ String hostname;
+ int port;
+
+ if (!host.contains(":")) {
+ hostname = host;
+ port = 1028;
+ } else {
+ String[] split = host.split(":", 2);
+ hostname = split[0].trim();
+ String p1 = split[1].trim();
+ if (hostname.isEmpty() || p1.isEmpty()) {
+ broker.invalidateCurrentInfoName();
+ throw new IllegalStateException("Invalid INS host:port value: " + host);
+ }
+ try {
+ port = Integer.parseInt(p1);
+ } catch (NumberFormatException e) {
+ broker.invalidateCurrentInfoName();
+ throw new IllegalStateException("Invalid port in INS record: " + host, e);
+ }
+ }
+
+ Thread t = new Thread(() -> connectServer(hostname, port), "oac-web-server-connect");
+ t.setDaemon(true);
+ t.start();
+
+ return;
+ }
+
+ if (p instanceof WebResponsePacket resp) {
+ onWebResponse(resp);
+ return;
+ }
+
+ if (p instanceof WebStreamStartPacket start) {
+ onStreamStart(start);
+ return;
+ }
+
+ if (p instanceof WebStreamChunkPacket chunk) {
+ onStreamChunk(chunk);
+ return;
+ }
+
+ if (p instanceof WebStreamEndPacket end) {
+ onStreamEnd(end);
+ }
+ }
+
+ private void connectServer(String hostname, int port) {
+ try {
+ // Ensure the server connection object exists
+ client.buildServerConnection(null, client.getProtocolBridge().getProtocolValues().ssl);
+
+ if (client.getClientServerConnection() != null && client.getClientServerConnection().isConnected()) {
+ client.getClientServerConnection().disconnect();
+ }
+
+ client.getClientServerConnection().connect(hostname, port);
+ } catch (Exception e) {
+ broker.invalidateCurrentInfoName();
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Handles a non-streamed WebResponsePacket.
+ *
+ * @param resp response packet
+ */
+ private void onWebResponse(WebResponsePacket resp) {
+ broker.onWebResponse(resp.getStatusCode(), resp.getContentType(), resp.getHeaders(), resp.getBody());
+ }
+
+ /**
+ * Handles the beginning of a streamed response.
+ *
+ * @param start stream start packet
+ */
+ private void onStreamStart(WebStreamStartPacket start) {
+ broker.onStreamStart(start.getStatusCode(), start.getContentType(), start.getHeaders(), start.getTotalLength());
+ }
+
+ /**
+ * Handles a chunk of a streamed response.
+ *
+ * @param chunk chunk packet
+ */
+ private void onStreamChunk(WebStreamChunkPacket chunk) {
+ broker.onStreamChunk(chunk.getSeq(), chunk.getData());
+ }
+
+ /**
+ * Handles stream end.
+ *
+ * @param end stream end packet
+ */
+ private void onStreamEnd(WebStreamEndPacket end) {
+ broker.onStreamEnd(end.isOk());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java
new file mode 100644
index 0000000..8a93315
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebRequestBroker.java
@@ -0,0 +1,419 @@
+package org.openautonomousconnection.infonamelib;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.INSQueryPacket;
+import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket;
+import org.openautonomousconnection.protocol.side.client.ProtocolClient;
+import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
+import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Central broker that translates {@code web://} URLs into OAC protocol traffic.
+ *
+ * Protocol limitation: no correlation id -> single-flight (one in-flight request at a time).
+ */
+public final class OacWebRequestBroker {
+
+ private static final OacWebRequestBroker INSTANCE = new OacWebRequestBroker();
+
+ private static final long CONNECT_TIMEOUT_SECONDS = 10;
+ private static final long RESPONSE_TIMEOUT_SECONDS = 25;
+ private final Object responseLock = new Object();
+ private volatile ProtocolClient client;
+ private volatile CountDownLatch connectionLatch;
+ private volatile String currentInfoName;
+ private volatile ResponseState responseState;
+
+ private OacWebRequestBroker() {
+ }
+
+ /**
+ * Returns the singleton broker.
+ *
+ * @return broker
+ */
+ public static OacWebRequestBroker get() {
+ return INSTANCE;
+ }
+
+ private static String safeContentType(String ct) {
+ return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
+ }
+
+ private static Map safeHeaders(Map headers) {
+ return (headers == null || headers.isEmpty()) ? Map.of() : Map.copyOf(headers);
+ }
+
+ private static String normalizePath(String path) {
+ if (path == null || path.isBlank() || "/".equals(path)) {
+ return "index.html";
+ }
+ String p = path.startsWith("/") ? path.substring(1) : path;
+ return p.isBlank() ? "index.html" : p;
+ }
+
+ /**
+ * Attaches the client used to send INS/Web packets.
+ *
+ * @param client protocol client
+ */
+ public void attachClient(ProtocolClient client) {
+ this.client = Objects.requireNonNull(client, "client");
+ }
+
+ /**
+ * Fetches a URL via OAC protocol (used by {@link java.net.URLConnection}).
+ *
+ * @param url web:// URL
+ * @return response
+ */
+ public OacWebResponse fetch(URL url) {
+ Objects.requireNonNull(url, "url");
+
+ ProtocolClient c = this.client;
+ if (c == null) {
+ throw new IllegalStateException("ProtocolClient not attached. Call OacWebUrlInstaller.installOnce(..., client) first.");
+ }
+
+ Response r = openAndAwait(c, url);
+
+ byte[] body = (r.body() == null) ? new byte[0] : r.body();
+ long len = body.length;
+
+ return new OacWebResponse(
+ r.statusCode(),
+ r.contentType(),
+ OacWebResponse.safeHeaders(r.headers()),
+ new ByteArrayInputStream(body),
+ len
+ );
+ }
+
+ /**
+ * Opens a resource and blocks until the current single-flight response completes.
+ *
+ * @param client protocol client
+ * @param url web:// URL
+ * @return response snapshot
+ */
+ public Response openAndAwait(ProtocolClient client, URL url) {
+ Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(url, "url");
+
+ open(client, url);
+ return awaitResponse(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Sends required packets for a {@code web://} URL.
+ *
+ * @param client protocol client
+ * @param url web:// URL
+ */
+ public synchronized void open(ProtocolClient client, URL url) {
+ Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(url, "url");
+
+ if (!"web".equalsIgnoreCase(url.getProtocol())) {
+ throw new IllegalArgumentException("Unsupported protocol: " + url.getProtocol());
+ }
+
+ String infoName = url.getHost();
+ if (infoName == null || infoName.isBlank()) {
+ throw new IllegalArgumentException("Missing InfoName in URL: " + url);
+ }
+
+ String path = normalizePath(url.getPath());
+
+ beginNewResponse();
+
+ if (!infoName.equals(currentInfoName)) {
+ resolveAndConnect(client, infoName);
+ currentInfoName = infoName;
+ } else {
+ awaitConnectionIfPending();
+ }
+
+ sendWebRequest(client, path);
+ }
+
+ /**
+ * Called once the ServerConnection is established (from listener).
+ */
+ public void notifyServerConnected() {
+ CountDownLatch latch = connectionLatch;
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ /**
+ * Forces re-resolution on next request.
+ */
+ public synchronized void invalidateCurrentInfoName() {
+ currentInfoName = null;
+ }
+
+ /**
+ * Receives non-streamed WebResponsePacket.
+ */
+ public void onWebResponse(int statusCode, String contentType, Map headers, byte[] body) {
+ ResponseState st = responseState;
+ if (st == null) return;
+
+ synchronized (responseLock) {
+ if (st.completed) return;
+
+ st.statusCode = statusCode;
+ st.contentType = safeContentType(contentType);
+ st.headers = safeHeaders(headers);
+
+ byte[] b = (body == null) ? new byte[0] : body;
+ st.body.write(b, 0, b.length);
+
+ st.completed = true;
+ st.success = true;
+ st.done.countDown();
+ }
+ }
+
+ /**
+ * Receives stream start.
+ */
+ public void onStreamStart(int statusCode, String contentType, Map headers, long totalLength) {
+ ResponseState st = responseState;
+ if (st == null) return;
+
+ synchronized (responseLock) {
+ if (st.completed) return;
+
+ st.statusCode = statusCode;
+ st.contentType = safeContentType(contentType);
+ st.headers = safeHeaders(headers);
+ st.totalLength = Math.max(0, totalLength);
+
+ st.streamStarted = true;
+ }
+ }
+
+ // ============================
+ // Internal connect + request
+ // ============================
+
+ /**
+ * Receives a stream chunk (may arrive out-of-order).
+ */
+ public void onStreamChunk(int seq, byte[] data) {
+ ResponseState st = responseState;
+ if (st == null) return;
+ if (data == null || data.length == 0) return;
+
+ synchronized (responseLock) {
+ if (st.completed) return;
+
+ if (!st.streamStarted) {
+ failLocked(st, "Stream chunk received before stream start");
+ return;
+ }
+
+ st.chunkBuffer.put(seq, Arrays.copyOf(data, data.length));
+ flushChunksLocked(st);
+ }
+ }
+
+ /**
+ * Receives stream end.
+ */
+ public void onStreamEnd(boolean ok) {
+ ResponseState st = responseState;
+ if (st == null) return;
+
+ synchronized (responseLock) {
+ if (st.completed) return;
+
+ if (!st.streamStarted) {
+ st.streamStarted = true;
+ }
+
+ flushChunksLocked(st);
+
+ st.completed = true;
+ st.success = ok;
+ if (!ok) {
+ st.errorMessage = "Streaming failed";
+ }
+ st.done.countDown();
+ }
+ }
+
+ /**
+ * Waits for the current response.
+ */
+ public Response awaitResponse(long timeout, TimeUnit unit) {
+ Objects.requireNonNull(unit, "unit");
+
+ ResponseState st = responseState;
+ if (st == null) {
+ throw new IllegalStateException("No in-flight request");
+ }
+
+ try {
+ boolean ok = st.done.await(timeout, unit);
+ if (!ok) {
+ throw new IllegalStateException("Timeout while waiting for Web response");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while waiting for Web response", e);
+ }
+
+ synchronized (responseLock) {
+ if (!st.success) {
+ throw new IllegalStateException(st.errorMessage == null ? "Request failed" : st.errorMessage);
+ }
+ return new Response(
+ st.statusCode,
+ st.contentType,
+ st.headers,
+ st.body.toByteArray()
+ );
+ }
+ }
+
+ // ============================
+ // Response helpers
+ // ============================
+
+ private void resolveAndConnect(ProtocolClient client, String infoName) {
+ if (client.getClientINSConnection() == null || !client.getClientINSConnection().isConnected()) return;
+ String[] parts = infoName.split("\\.");
+ if (parts.length < 2 || parts.length > 3) {
+ throw new IllegalArgumentException(
+ "Invalid INS address format: " + infoName + " (expected name.tln or sub.name.tln)"
+ );
+ }
+
+ String tln = parts[parts.length - 1];
+ String name = parts[parts.length - 2];
+ String sub = (parts.length == 3) ? parts[0] : null;
+
+ connectionLatch = new CountDownLatch(1);
+
+ INSQueryPacket query = new INSQueryPacket(
+ tln,
+ name,
+ sub,
+ INSRecordType.A,
+ client.getClientINSConnection().getUniqueID()
+ );
+
+ try {
+ client.getClientINSConnection().sendPacket(query, TransportProtocol.TCP);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to send INSQueryPacket for " + infoName, e);
+ }
+
+ awaitConnectionIfPending();
+ }
+
+ private void awaitConnectionIfPending() {
+ CountDownLatch latch = connectionLatch;
+ if (latch == null || client.getClientServerConnection() == null || client.getClientINSConnection() == null)
+ return;
+
+ try {
+ if (!latch.await(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timeout while waiting for ServerConnection");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while waiting for ServerConnection", e);
+ }
+ }
+
+ private void sendWebRequest(ProtocolClient client, String path) {
+ Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(path, "path");
+
+ if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
+ awaitConnectionIfPending();
+ }
+
+ if (client.getClientServerConnection() == null || !client.getClientServerConnection().isConnected()) {
+ throw new IllegalStateException("ServerConnection is not connected after waiting");
+ }
+
+ WebRequestPacket packet = new WebRequestPacket(
+ path,
+ WebRequestMethod.GET,
+ Map.of(),
+ null
+ );
+
+ try {
+ client.getClientServerConnection().sendPacket(packet, TransportProtocol.TCP);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to send WebRequestPacket for path " + path, e);
+ }
+ }
+
+ private void beginNewResponse() {
+ synchronized (responseLock) {
+ responseState = new ResponseState();
+ }
+ }
+
+ private void flushChunksLocked(ResponseState st) {
+ for (; ; ) {
+ byte[] next = st.chunkBuffer.remove(st.nextExpectedSeq);
+ if (next == null) break;
+
+ st.body.write(next, 0, next.length);
+ st.nextExpectedSeq++;
+ }
+ }
+
+ private void failLocked(ResponseState st, String message) {
+ st.completed = true;
+ st.success = false;
+ st.errorMessage = message;
+ st.done.countDown();
+ }
+
+ /**
+ * Immutable response snapshot.
+ *
+ * @param statusCode status code
+ * @param contentType content type
+ * @param headers headers
+ * @param body body bytes
+ */
+ public record Response(int statusCode, String contentType, Map headers, byte[] body) {
+ }
+
+ private static final class ResponseState {
+ private final CountDownLatch done = new CountDownLatch(1);
+ private final ByteArrayOutputStream body = new ByteArrayOutputStream(64 * 1024);
+ private final Map chunkBuffer = new HashMap<>();
+ private int statusCode = 0;
+ private String contentType = "application/octet-stream";
+ private Map headers = Map.of();
+ private boolean streamStarted;
+ private long totalLength;
+ private int nextExpectedSeq = 0;
+
+ private boolean completed;
+ private boolean success;
+ private String errorMessage;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java
new file mode 100644
index 0000000..208fc7b
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebResponse.java
@@ -0,0 +1,32 @@
+package org.openautonomousconnection.infonamelib;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents a resolved web response for the JavaFX WebView.
+ *
+ * @param statusCode HTTP-like status code
+ * @param contentType response content-type (as sent by server)
+ * @param headers response headers
+ * @param bodyStream body stream (may be streaming)
+ * @param contentLength content length if known, else -1
+ */
+public record OacWebResponse(
+ int statusCode,
+ String contentType,
+ Map headers,
+ InputStream bodyStream,
+ long contentLength
+) {
+ public OacWebResponse {
+ Objects.requireNonNull(headers, "headers");
+ Objects.requireNonNull(bodyStream, "bodyStream");
+ }
+
+ public static Map safeHeaders(Map h) {
+ return (h == null) ? Collections.emptyMap() : Collections.unmodifiableMap(h);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java
new file mode 100644
index 0000000..a909d87
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebURLConnection.java
@@ -0,0 +1,126 @@
+package org.openautonomousconnection.infonamelib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.*;
+
+/**
+ * URLConnection implementation that maps "web://" URLs to OAC WebRequestPacket/WebResponsePacket.
+ *
+ * Important: This connection enforces global serialization of requests because the protocol has no request IDs.
+ */
+public final class OacWebURLConnection extends URLConnection {
+
+ private static final int MAX_REDIRECTS = 8;
+
+ private final OacWebRequestBroker broker;
+
+ private boolean connected;
+ private OacWebResponse response;
+
+ public OacWebURLConnection(URL url, OacWebRequestBroker broker) {
+ super(url);
+ this.broker = Objects.requireNonNull(broker, "broker");
+ }
+
+ private static String headerValue(Map headers, String nameLower) {
+ if (headers == null || headers.isEmpty()) return null;
+ for (Map.Entry e : headers.entrySet()) {
+ if (e.getKey() == null) continue;
+ if (e.getKey().trim().toLowerCase(Locale.ROOT).equals(nameLower)) {
+ return e.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void connect() throws IOException {
+ if (connected) return;
+
+ URL cur = this.url;
+ OacWebResponse resp = null;
+
+ for (int i = 0; i <= MAX_REDIRECTS; i++) {
+ resp = broker.fetch(cur);
+
+ int code = resp.statusCode();
+ if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
+ String loc = headerValue(resp.headers(), "location");
+ if (loc == null || loc.isBlank()) {
+ break;
+ }
+ try {
+ cur = new URL(cur, loc);
+ continue;
+ } catch (Exception ex) {
+ break;
+ }
+ }
+
+ // Non-redirect or redirect that cannot be followed -> stop here.
+ break;
+ }
+
+ this.response = resp;
+ this.connected = true;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ connect();
+ return response.bodyStream();
+ }
+
+ @Override
+ public String getContentType() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return "application/octet-stream";
+ }
+ String ct = response.contentType();
+ return (ct == null || ct.isBlank()) ? "application/octet-stream" : ct;
+ }
+
+ @Override
+ public int getContentLength() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return -1;
+ }
+ long len = response.contentLength();
+ return (len <= 0 || len > Integer.MAX_VALUE) ? -1 : (int) len;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return -1L;
+ }
+ return response.contentLength();
+ }
+
+ @Override
+ public Map> getHeaderFields() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return Map.of();
+ }
+
+ Map> out = new LinkedHashMap<>();
+ for (Map.Entry e : response.headers().entrySet()) {
+ String k = e.getKey();
+ String v = e.getValue();
+ if (k == null) continue;
+ out.put(k, v == null ? List.of() : List.of(v));
+ }
+ return out;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java
new file mode 100644
index 0000000..7c5a0b2
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlInstaller.java
@@ -0,0 +1,52 @@
+package org.openautonomousconnection.infonamelib;
+
+import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
+import org.openautonomousconnection.protocol.side.client.ProtocolClient;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Installs the "web://" protocol handler using the standard Java mechanism:
+ * {@code java.protocol.handler.pkgs}.
+ *
+ * This avoids {@link java.net.URL#setURLStreamHandlerFactory} which can only be set once per JVM.
+ */
+public final class OacWebUrlInstaller {
+
+ private static final AtomicBoolean INSTALLED = new AtomicBoolean(false);
+
+ private OacWebUrlInstaller() {
+ }
+
+ /**
+ * Installs:
+ *
+ * - protocol handler package prefix
+ * - packet listener forwarding WebResponse/WebStream + INSResponse into the broker
+ *
+ *
+ * Must be called before any {@code web://} URL is resolved/loaded.
+ *
+ * @param eventManager global event manager
+ * @param client protocol client (required for connecting ServerConnection after INS resolution)
+ */
+ public static void installOnce(EventManager eventManager, ProtocolClient client) {
+ Objects.requireNonNull(eventManager, "eventManager");
+ Objects.requireNonNull(client, "client");
+
+ if (!INSTALLED.compareAndSet(false, true)) {
+ return;
+ }
+
+ // Make client available for broker.fetch(...)
+ OacWebRequestBroker.get().attachClient(client);
+
+ // Register packet listener (INSResponse + WebResponse + WebStream*)
+ eventManager.registerListener(new OacWebPacketListener(OacWebRequestBroker.get(), client));
+
+ // Register protocol handler package prefix:
+ // JVM will load: "org.openautonomousconnection.webclient.recode.url.web.Handler"
+ ProtocolHandlerPackages.installPackage("org.openautonomousconnection.webclient.recode.url");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java
new file mode 100644
index 0000000..22d311e
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/OacWebUrlStreamHandler.java
@@ -0,0 +1,28 @@
+package org.openautonomousconnection.infonamelib;
+
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.util.Objects;
+
+/**
+ * URLStreamHandler for the custom OAC scheme "web://".
+ */
+public final class OacWebUrlStreamHandler extends URLStreamHandler {
+
+ private final OacWebRequestBroker broker;
+
+ /**
+ * Creates a handler.
+ *
+ * @param broker request broker
+ */
+ public OacWebUrlStreamHandler(OacWebRequestBroker broker) {
+ this.broker = Objects.requireNonNull(broker, "broker");
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) {
+ return new OacWebURLConnection(u, broker);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java b/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java
new file mode 100644
index 0000000..a51dd14
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/ProtocolHandlerPackages.java
@@ -0,0 +1,42 @@
+package org.openautonomousconnection.infonamelib;
+
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility to safely append packages to "java.protocol.handler.pkgs".
+ */
+public final class ProtocolHandlerPackages {
+
+ private static final String KEY = "java.protocol.handler.pkgs";
+
+ private ProtocolHandlerPackages() {
+ }
+
+ /**
+ * Appends a package prefix to the protocol handler search path.
+ *
+ * @param pkg package prefix (e.g. "com.example.protocols")
+ */
+ public static void installPackage(String pkg) {
+ Objects.requireNonNull(pkg, "pkg");
+ String p = pkg.trim();
+ if (p.isEmpty()) return;
+
+ String existing = System.getProperty(KEY, "");
+ Set parts = new LinkedHashSet<>();
+
+ if (!existing.isBlank()) {
+ for (String s : existing.split("\\|")) {
+ String t = s.trim();
+ if (!t.isEmpty()) parts.add(t);
+ }
+ }
+
+ parts.add(p);
+
+ String merged = String.join("|", parts);
+ System.setProperty(KEY, merged);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java
deleted file mode 100644
index cde274e..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLConnection.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib.protocols.ftp;
-
-import java.io.IOException;
-import java.net.URL;
-import java.net.URLConnection;
-
-public class FTPInfoNameURLConnection extends URLConnection {
- /**
- * Constructs a URL connection to the specified URL. A connection to
- * the object referenced by the URL is not created.
- *
- * @param url the specified URL.
- */
- protected FTPInfoNameURLConnection(URL url) {
- super(url);
- }
-
- @Override
- public void connect() throws IOException {
-
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java
deleted file mode 100644
index ab6841f..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/ftp/FTPInfoNameURLStreamHandler.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib.protocols.ftp;
-
-import java.io.IOException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
-
-public class FTPInfoNameURLStreamHandler extends URLStreamHandler {
- @Override
- protected URLConnection openConnection(URL url) throws IOException {
- return new FTPInfoNameURLConnection(url);
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java
deleted file mode 100644
index 8f6872c..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLConnection.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib.protocols.web;
-
-import java.io.IOException;
-import java.net.URL;
-import java.net.URLConnection;
-
-public class WebInfoNameURLConnection extends URLConnection {
- /**
- * Constructs a URL connection to the specified URL. A connection to
- * the object referenced by the URL is not created.
- *
- * @param url the specified URL.
- */
- protected WebInfoNameURLConnection(URL url) {
- super(url);
- }
-
- @Override
- public void connect() throws IOException {
-
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java b/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java
deleted file mode 100644
index 9930204..0000000
--- a/src/main/java/org/openautonomousconnection/infonamelib/protocols/web/WebInfoNameURLStreamHandler.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* Author: Maple
- * Jan. 18 2026
- * */
-
-package org.openautonomousconnection.infonamelib.protocols.web;
-
-import java.io.IOException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
-
-public class WebInfoNameURLStreamHandler extends URLStreamHandler {
- @Override
- protected URLConnection openConnection(URL url) throws IOException {
- return new WebInfoNameURLConnection(url);
- }
-}
diff --git a/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java b/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java
new file mode 100644
index 0000000..b08b564
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/infonamelib/web/Handler.java
@@ -0,0 +1,22 @@
+package org.openautonomousconnection.infonamelib.web;
+
+import org.openautonomousconnection.infonamelib.OacWebRequestBroker;
+import org.openautonomousconnection.infonamelib.OacWebURLConnection;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * URLStreamHandler for the "web" protocol.
+ *
+ * Loaded via the "java.protocol.handler.pkgs" mechanism.
+ */
+public final class Handler extends URLStreamHandler {
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return new OacWebURLConnection(u, OacWebRequestBroker.get());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/license/protocol/oapl b/src/main/resources/license/protocol/oapl
new file mode 100644
index 0000000..3f64855
--- /dev/null
+++ b/src/main/resources/license/protocol/oapl
@@ -0,0 +1 @@
+Please read the license here: https://open-autonomous-connection.org/license.html
\ No newline at end of file