Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
10d71ba
added initial webclient API that mirrors the Webserver API and uses t…
jurgenvinju May 11, 2026
645d334
cleanup and added POST method
jurgenvinju May 11, 2026
61cadea
added the other methods
jurgenvinju May 11, 2026
6cce6de
added progress bar
jurgenvinju May 11, 2026
17723ff
fixed post
jurgenvinju May 12, 2026
b9c1e6e
constructor typo
jurgenvinju May 12, 2026
8c9c0da
fix post bug
jurgenvinju May 12, 2026
ddf4bfa
added path to other requests kinds but GET and POST
jurgenvinju May 12, 2026
6b37e74
Merge branch 'main' into feat/webclient
jurgenvinju May 19, 2026
53ae8a0
started rewrite of Server and Client interface to canonically treat a…
jurgenvinju May 19, 2026
cd8bdda
big cleanup of Webclient, but Webserver is broken now and I still hav…
jurgenvinju May 21, 2026
8c51344
debugging with @davylandman
jurgenvinju May 21, 2026
748dcc7
linked up the Subscription API as well to complete the stream
jurgenvinju May 22, 2026
ae4eefe
improving error handling of common mistakes in the client
jurgenvinju May 22, 2026
9f7b1b5
factored out Writer-based suppliers
jurgenvinju May 22, 2026
06b20ad
added asserts to diagnose possible race
jurgenvinju May 22, 2026
e1f8367
error handling for bad URLs
jurgenvinju May 22, 2026
76abafe
removed dead use of parameter
jurgenvinju May 22, 2026
bee60e8
this seems to have fixed the race
jurgenvinju May 22, 2026
345585e
comments
jurgenvinju May 22, 2026
548aac6
deal with null messages of IOException generally
jurgenvinju May 22, 2026
db4c552
fixed off-by-one in download progress
jurgenvinju May 23, 2026
0ad2cb2
rewrote the webserver side to accept the new Body constructors send a…
jurgenvinju May 26, 2026
c1ffd9e
server is working again
jurgenvinju May 27, 2026
0f450c2
added xml and html sending and receiving, only server side reception …
jurgenvinju May 27, 2026
73f21f0
refactoring that factors common WriterToInputStream functionality int…
jurgenvinju May 27, 2026
f9416a0
fixing issues with module management in client and server
jurgenvinju May 28, 2026
7bc2ee5
rationalized JSON options
jurgenvinju May 28, 2026
50faa40
cleanup of Webservice module and added some example usages
jurgenvinju May 28, 2026
111d4f4
linked charset parameter of POST and PUT bodies
jurgenvinju May 28, 2026
8123624
finished charset and mimetype propagation to POSt and PUT headers
jurgenvinju May 28, 2026
c969047
wired server side mimeType and response for bodies
jurgenvinju May 28, 2026
6a047d3
fixed copy/paste bug
jurgenvinju May 28, 2026
c0311de
fixed switch case missing break bug
jurgenvinju May 28, 2026
58c5ff2
added missing file
jurgenvinju May 28, 2026
49c6a4e
added more headers
jurgenvinju May 28, 2026
b9a1c6a
Merge branch 'main' into feat/webclient
jurgenvinju May 28, 2026
c9134db
fixing more comments by @davylandman
jurgenvinju May 28, 2026
fe1b7a0
wrote a test server that does not lock the interpreter but does use a…
jurgenvinju May 28, 2026
6869703
some fixes. not yet working
jurgenvinju May 28, 2026
0304c83
test servers are working
jurgenvinju May 29, 2026
9018c05
writing first good tests. they still fail
jurgenvinju May 29, 2026
223ea5e
fixing things one-by-one
jurgenvinju May 29, 2026
9953b5e
fixed sloppy NPEs
jurgenvinju May 29, 2026
98e32c4
fixed content-type for POST and PUT bodies in client
jurgenvinju May 29, 2026
e4f2434
big rewrite to Undertow modern HTTP server in Java, removed clone of …
jurgenvinju Jun 1, 2026
97fca4d
cleaning up
jurgenvinju Jun 1, 2026
6573779
one more compilation error
jurgenvinju Jun 1, 2026
3021254
fixed some warnings to get overview back
jurgenvinju Jun 1, 2026
0b96380
bumped to Java 17 because Undertow requires it
jurgenvinju Jun 1, 2026
6fb2d38
solved issues with new yield keyword in Java 17
jurgenvinju Jun 1, 2026
9464ac0
bumped action workflow to Java 17
jurgenvinju Jun 1, 2026
d3031ad
fixing stuff after jump to Java 17
jurgenvinju Jun 1, 2026
f86e26e
some more fixes to get the server working again. almost there
jurgenvinju Jun 1, 2026
2dd1a67
fixes and cleanup
jurgenvinju Jun 2, 2026
f07d333
can call test server with builtin client
jurgenvinju Jun 2, 2026
d008c72
fixed sending and receiving HTMLElement instances over http (client/s…
jurgenvinju Jun 2, 2026
78ddeef
added XML roundtrip testing
jurgenvinju Jun 2, 2026
faec97e
fixed XML roundtripping
jurgenvinju Jun 2, 2026
aa64b22
added HTMLOptions for later use by requests
jurgenvinju Jun 3, 2026
3f2dcb0
rationalized and finished JSON options
jurgenvinju Jun 3, 2026
e45b380
threaded HTML options
jurgenvinju Jun 3, 2026
bb3ffd0
streamed XML options too
jurgenvinju Jun 3, 2026
db2fbfa
added xmlOptions to BodyKind
jurgenvinju Jun 3, 2026
434fcea
enabled automatic port allocation for util::Webserver
jurgenvinju Jun 3, 2026
155ebaa
added precision parameter
jurgenvinju Jun 3, 2026
1e91b7f
added precision parameter
jurgenvinju Jun 3, 2026
2ace0d9
workaround
jurgenvinju Jun 3, 2026
9219d22
a rather difficult merge
jurgenvinju Jun 3, 2026
07539c0
Merge branch 'main' into feat/webclient
jurgenvinju Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/org/rascalmpl/library/Content.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ data Request (map[str, str] headers = (), map[str, str] parameters = (), map[str
| head(str path)
;


@synopsis{A response encodes what is send back from the server to the browser client.}
@description{
The three kinds of responses, encode either content that is already a `str`,
Expand Down
368 changes: 368 additions & 0 deletions src/org/rascalmpl/library/util/Webclient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
package org.rascalmpl.library.util;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.StringWriter;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.util.stream.Collectors;
import java.io.Writer;
import org.rascalmpl.debug.IRascalMonitor;
import org.rascalmpl.exceptions.RuntimeExceptionFactory;
import org.rascalmpl.library.Prelude;
import org.rascalmpl.library.lang.json.internal.JsonValueReader;
import org.rascalmpl.library.lang.json.internal.JsonValueWriter;
import org.rascalmpl.types.TypeReifier;
import org.rascalmpl.uri.URIResolverRegistry;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.values.IRascalValueFactory;
import org.rascalmpl.values.functions.IFunction;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import fi.iki.elonen.NanoHTTPD.Response.Status;
import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.ISourceLocation;
import io.usethesource.vallang.IString;
import io.usethesource.vallang.type.Type;
import io.usethesource.vallang.type.TypeFactory;
import io.usethesource.vallang.type.TypeStore;

public class Webclient {
private final IRascalValueFactory vf;
private final IRascalMonitor monitor;
private final TypeStore store;
private final TypeFactory tf;
private final TypeReifier tr;

public Webclient(IRascalValueFactory vf, IRascalMonitor monitor, TypeStore store, TypeFactory tf) {
this.vf = vf;
this.monitor = monitor;
this.store = store;
this.tf = tf;
this.tr = new TypeReifier(vf);
}

private HttpRequest makeGetRequest(IConstructor input) {
var params = input.asWithKeywordParameters();
var host = ((ISourceLocation) params.getParameter("host"));
host = host == null ? URIUtil.assumeCorrectLocation("http://www.example.com") : host;
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
var path = ((IString) input.get("path")).getValue();

return HttpRequest.newBuilder()
.uri(URIUtil.getChildLocation(host, path).getURI())
.GET()
.build();
}

private HttpRequest makePutRequest(IConstructor input) {
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
var params = input.asWithKeywordParameters();
var postBody = (IFunction) input.get("body");
var rt = new TypeReifier(vf).typeToValue(tf.stringType(), store, vf.map());
var host = ((ISourceLocation) params.getParameter("host"));
host = host == null ? URIUtil.assumeCorrectLocation("http://www.example.com") : host;
var path = ((IString) input.get("path")).getValue();

return HttpRequest.newBuilder()
.uri(URIUtil.getChildLocation(host, path).getURI())
.PUT(HttpRequest.BodyPublishers.ofString(((IString) postBody.call(rt)).getValue()))
.build();
}

private HttpRequest makeDeleteRequest(IConstructor input) {
var params = input.asWithKeywordParameters();
var host = ((ISourceLocation) params.getParameter("host"));
host = host == null ? URIUtil.assumeCorrectLocation("http://www.example.com") : host;
var path = ((IString) input.get("path")).getValue();

return HttpRequest.newBuilder()
.uri(URIUtil.getChildLocation(host, path).getURI())
.DELETE()
.build();
}

private HttpRequest makeHeadRequest(IConstructor input) {
var params = input.asWithKeywordParameters();
var host = ((ISourceLocation) params.getParameter("host"));
host = host == null ? URIUtil.assumeCorrectLocation("http://www.example.com") : host;
var path = ((IString) input.get("path")).getValue();

return HttpRequest.newBuilder()
.uri(URIUtil.getChildLocation(host, path).getURI())
.method("HEAD", BodyPublishers.noBody())
.build();
}

private HttpRequest makePostRequest(IConstructor input) {
var params = input.asWithKeywordParameters();
var postBody = (IFunction) input.get("content");
var rt = new TypeReifier(vf).typeToValue(tf.valueType(), store, vf.map());
var host = ((ISourceLocation) params.getParameter("host"));
var val = postBody.call(rt);
var path = ((IString) input.get("path")).getValue();

try {
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out, 64 * 1024);

Thread writer = new Thread(() -> {
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
try (OutputStream os = out; Writer w = new OutputStreamWriter(out)) {
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
JsonWriter jsonWriter = new JsonWriter(w);
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
JsonValueWriter jsonOut = new JsonValueWriter();
jsonOut.write(jsonWriter, val);
}
catch (Exception e) {
throw RuntimeExceptionFactory.io(e.getMessage());
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
}
});

writer.start();
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated

Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
return HttpRequest.newBuilder()
.uri(URIUtil.getChildLocation(host, path).getURI())
.POST(HttpRequest.BodyPublishers.ofInputStream(() -> in))
.build();
}
catch (IOException e) {
throw RuntimeExceptionFactory.io(e.getMessage());
}
}

private HttpRequest makeRequest(IConstructor input) {
switch (input.getName()) {
case "get":
return makeGetRequest(input);
case "post":
return makePostRequest(input);
case "put":
return makePutRequest(input);
case "delete":
return makeDeleteRequest(input);
case "head":
return makeHeadRequest(input);

default:
throw RuntimeExceptionFactory.illegalArgument(input);
}
}

/**
* This is the main API method for the Rascal side
*/
public IConstructor fetch(IConstructor input) {
try {
var request = makeRequest(input);
var response = HttpClient
.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
Comment thread
jurgenvinju marked this conversation as resolved.
.send(request, HttpResponse.BodyHandlers.ofInputStream());
return translateResponse(request.uri().toString(), (IConstructor) input.asWithKeywordParameters().getParameter("body"), response);
}
catch (IOException | InterruptedException e) {
throw RuntimeExceptionFactory.io(e.getMessage());
}
}

private IConstructor translateResponse(String url, IConstructor expect, HttpResponse<InputStream> response) throws IOException {
var headers = response
.headers()
.map()
.entrySet()
.stream()
.map(e -> vf.tuple(
vf.string(e.getKey()),
vf.string(e.getValue().stream().collect(Collectors.joining(","))
)))
.collect(vf.mapWriter());

long totalBytes = response.headers()
.firstValueAsLong("Content-Length")
.orElse(-1);

var input = totalBytes > 0
? new MonitoredInputStream(response.body(), monitor, "Fetching " + url, totalBytes)
: response.body();

var contentType = response.headers().firstValue("Content-Type");

var mimeType = vf.string(contentType.get().split(";")[0]);

// TODO: extract from contentType if present
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
var charset = "utf-8";

var status = toStatusConstructor(response.statusCode());

Type respCons;
IString body;

switch (expect != null ? expect.getName() : "textBody") {
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
case "jsonBody":
JsonReader jsonReader = new JsonReader(new InputStreamReader(input));
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
JsonValueReader parser = new JsonValueReader(vf, store, monitor, URIUtil.assumeCorrectLocation(url));
respCons = store.lookupConstructors("jsonResponse").iterator().next();
var value = parser.read(jsonReader, tr.valueToType((IConstructor) expect.get("expected")));
return vf.constructor(respCons, status, headers, value);
case "fileBody":
respCons = store.lookupConstructors("fileResponse").iterator().next();
var loc = (ISourceLocation) expect.get("storage");
try (OutputStream out = URIResolverRegistry.getInstance().getOutputStream(loc, false)) {
input.transferTo(out);
}
return vf.constructor(respCons, loc, mimeType, headers);
case "textBody":
default:
respCons = store.lookupConstructors("response").iterator().next();
body = vf.string(new String(Prelude.consumeInputStream(input), charset));
Comment thread
jurgenvinju marked this conversation as resolved.
Outdated
return vf.constructor(respCons, status, mimeType, headers, body);
}
}

private IConstructor toStatusConstructor(int stCode) {
var statusType = store.lookupAbstractDataType("Status");

var status = Status.lookup(stCode);
switch (status) {
case OK:
return vf.constructor(store.lookupConstructor(statusType, "ok", tf.tupleEmpty()));
case NOT_FOUND:
return vf.constructor(store.lookupConstructor(statusType, "notFound", tf.tupleEmpty()));
case ACCEPTED:
return vf.constructor(store.lookupConstructor(statusType, "accepted", tf.tupleEmpty()));
case BAD_REQUEST:
return vf.constructor(store.lookupConstructor(statusType, "badRequest", tf.tupleEmpty()));
case CONFLICT:
return vf.constructor(store.lookupConstructor(statusType, "conflict", tf.tupleEmpty()));
case CREATED:
return vf.constructor(store.lookupConstructor(statusType, "create", tf.tupleEmpty()));
case EXPECTATION_FAILED:
return vf.constructor(store.lookupConstructor(statusType, "expectationFailed", tf.tupleEmpty()));
case FORBIDDEN:
return vf.constructor(store.lookupConstructor(statusType, "forbidden", tf.tupleEmpty()));
case FOUND:
return vf.constructor(store.lookupConstructor(statusType, "found", tf.tupleEmpty()));
case GONE:
return vf.constructor(store.lookupConstructor(statusType, "gone", tf.tupleEmpty()));
case INTERNAL_ERROR:
return vf.constructor(store.lookupConstructor(statusType, "internalError", tf.tupleEmpty()));
case LENGTH_REQUIRED:
return vf.constructor(store.lookupConstructor(statusType, "lengthRequired", tf.tupleEmpty()));
case METHOD_NOT_ALLOWED:
return vf.constructor(store.lookupConstructor(statusType, "methodNotAllowed", tf.tupleEmpty()));
case MULTI_STATUS:
return vf.constructor(store.lookupConstructor(statusType, "multiStatus", tf.tupleEmpty()));
case NOT_ACCEPTABLE:
return vf.constructor(store.lookupConstructor(statusType, "notAcceptible", tf.tupleEmpty()));
case NOT_IMPLEMENTED:
return vf.constructor(store.lookupConstructor(statusType, "notImplemented", tf.tupleEmpty()));
case NOT_MODIFIED:
return vf.constructor(store.lookupConstructor(statusType, "notModified", tf.tupleEmpty()));
case NO_CONTENT:
return vf.constructor(store.lookupConstructor(statusType, "noContent", tf.tupleEmpty()));
case PARTIAL_CONTENT:
return vf.constructor(store.lookupConstructor(statusType, "partialContent", tf.tupleEmpty()));
case PAYLOAD_TOO_LARGE:
return vf.constructor(store.lookupConstructor(statusType, "payloadTooLarge", tf.tupleEmpty()));
case PRECONDITION_FAILED:
return vf.constructor(store.lookupConstructor(statusType, "preconditionFailed", tf.tupleEmpty()));
case RANGE_NOT_SATISFIABLE:
return vf.constructor(store.lookupConstructor(statusType, "rangeNotSatisfieable", tf.tupleEmpty()));
case REDIRECT:
return vf.constructor(store.lookupConstructor(statusType, "redirect", tf.tupleEmpty()));
case REDIRECT_SEE_OTHER:
return vf.constructor(store.lookupConstructor(statusType, "redirectSeeOther", tf.tupleEmpty()));
case REQUEST_TIMEOUT:
return vf.constructor(store.lookupConstructor(statusType, "requestTimeout", tf.tupleEmpty()));
case SERVICE_UNAVAILABLE:
return vf.constructor(store.lookupConstructor(statusType, "serviceUnavailable", tf.tupleEmpty()));
case SWITCH_PROTOCOL:
return vf.constructor(store.lookupConstructor(statusType, "switchProtocol", tf.tupleEmpty()));
case TEMPORARY_REDIRECT:
return vf.constructor(store.lookupConstructor(statusType, "temporaryRedirect", tf.tupleEmpty()));
case TOO_MANY_REQUESTS:
return vf.constructor(store.lookupConstructor(statusType, "tooManyRequests", tf.tupleEmpty()));
case UNAUTHORIZED:
return vf.constructor(store.lookupConstructor(statusType, "unauthorized", tf.tupleEmpty()));
case UNSUPPORTED_HTTP_VERSION:
return vf.constructor(store.lookupConstructor(statusType, "unsupportedHTTPVersion", tf.tupleEmpty()));
case UNSUPPORTED_MEDIA_TYPE:
return vf.constructor(store.lookupConstructor(statusType, "unsupportedMediaType", tf.tupleEmpty()));
default:
// if we don't understand the error code; let's call it an internal error
return vf.constructor(store.lookupConstructor(statusType, "internalError", tf.tupleEmpty()));
}
}

private class MonitoredInputStream extends FilterInputStream {
private final IRascalMonitor monitor;
private final String jobName;

private final long totalBytes;
private long bytesRead = 0;
private boolean started = false;
private boolean done = false;

public MonitoredInputStream(InputStream in, IRascalMonitor monitor, String jobName, long totalBytes) {
super(in);
this.totalBytes = totalBytes;
this.monitor = monitor;
this.jobName = jobName;
}

private void ensureStarted() {
if (!started) {
started = true;
monitor.jobStart(jobName, Integer.MAX_VALUE);
}
}

private void updateProgress(int bytesRead) throws InterruptedIOException {
if (monitor.jobIsCanceled(jobName)) {
throw new InterruptedIOException(jobName);
}

ensureStarted();
long numberOfTheseSteps = (int) (totalBytes / bytesRead);
int stepSize = (int) (Integer.MAX_VALUE / numberOfTheseSteps);
monitor.jobStep(jobName, "", java.lang.Math.max(stepSize, 1));
checkDone();
}

private void checkDone() {
if (!done && bytesRead >= totalBytes) {
done = true;
monitor.jobEnd(jobName, true);
}
}

@Override
public int read() throws IOException {
int b = super.read();
if (b != -1) {
bytesRead += 1;
updateProgress(1);
}
return b;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = super.read(b, off, len);
if (n > 0) {
bytesRead += n;
updateProgress(n);
}
return n;
}
}
}
Loading
Loading