diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 389ae13b1db..e9061236d63 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -84,6 +84,7 @@ public class GsonUtils { */ static { writer.setRationalsAsString(true); + writer.setFileLocationsAsPathOnly(false); typeMappings = List.of( new TypeMapping(IBool.class, tf.boolType()), new TypeMapping(ICollection.class), // IList, IMap, ISet diff --git a/src/org/rascalmpl/library/Content.rsc b/src/org/rascalmpl/library/Content.rsc index f7615a63368..9a8299f380e 100644 --- a/src/org/rascalmpl/library/Content.rsc +++ b/src/org/rascalmpl/library/Content.rsc @@ -82,7 +82,7 @@ data Response = response(Status status, str mimeType, map[str,str] header, str content) | fileResponse(loc file, str mimeType, map[str,str] header) | jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; }, - bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false) + bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false, bool fileLocationsAsPathOnly=true) ; @synopsis{Utility to quickly render a string as HTML content} diff --git a/src/org/rascalmpl/library/lang/json/IO.java b/src/org/rascalmpl/library/lang/json/IO.java index e5b91d89fda..7075077564d 100644 --- a/src/org/rascalmpl/library/lang/json/IO.java +++ b/src/org/rascalmpl/library/lang/json/IO.java @@ -106,7 +106,7 @@ public IValue parseJSON(IValue type, IString src, IString dateTimeFormat, IBool public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, - IBool explicitDataTypes) { + IBool explicitDataTypes, IBool fileLocationsAsPathOnly) { try (JsonWriter out = new JsonWriter(new OutputStreamWriter(URIResolverRegistry.getInstance().getOutputStream(loc, false), Charset.forName("UTF8")))) { @@ -123,6 +123,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations .setFormatters(formatter) .setExplicitConstructorNames(explicitConstructorNames.getValue()) .setExplicitDataTypes(explicitDataTypes.getValue()) + .setFileLocationsAsPathOnly(fileLocationsAsPathOnly.getValue()) .write(out, value); } catch (IOException e) { @@ -132,7 +133,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, - IBool explicitDataTypes) { + IBool explicitDataTypes, IBool fileLocationsAsPathOnly) { StringWriter string = new StringWriter(); try (JsonWriter out = new JsonWriter(string)) { @@ -148,6 +149,7 @@ public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFor .setFormatters(formatter) .setExplicitConstructorNames(explicitConstructorNames.getValue()) .setExplicitDataTypes(explicitDataTypes.getValue()) + .setFileLocationsAsPathOnly(fileLocationsAsPathOnly.getValue()) .write(out, value); return values.string(string.toString()); diff --git a/src/org/rascalmpl/library/lang/json/IO.rsc b/src/org/rascalmpl/library/lang/json/IO.rsc index 7b177097029..b5c5066508e 100644 --- a/src/org/rascalmpl/library/lang/json/IO.rsc +++ b/src/org/rascalmpl/library/lang/json/IO.rsc @@ -175,7 +175,8 @@ java void writeJSON(loc target, value val, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, - bool explicitDataTypes=false + bool explicitDataTypes=false, + bool fileLocationsAsPathOnly=true ); @javaClass{org.rascalmpl.library.lang.json.IO} @@ -184,7 +185,7 @@ java void writeJSON(loc target, value val, @description{ This function uses `writeJSON` and stores the result in a string. } -java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, bool rationalsAsString=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false); +java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, bool rationalsAsString=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false, bool fileLocationsAsPathOnly=true); @synopsis{((writeJSON)) and ((asJSON)) uses `Formatter` functions to flatten structured data to strings, on-demand} @description{ diff --git a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java index ad7e3eea891..3ec2e3d1fab 100644 --- a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java +++ b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java @@ -67,6 +67,7 @@ public class JsonValueWriter { private IFunction formatters; private boolean explicitConstructorNames = false; private boolean explicitDataTypes; + private boolean fileLocationsAsPathOnly = true; /** helper class for number serialization without quotes */ private static class RascalNumber extends Number { @@ -163,6 +164,11 @@ public JsonValueWriter setExplicitDataTypes(boolean setting) { return this; } + public JsonValueWriter setFileLocationsAsPathOnly(boolean setting) { + this.fileLocationsAsPathOnly = setting; + return this; + } + public void write(JsonWriter out, IValue value) throws IOException { value.accept(new IValueVisitor() { @@ -262,7 +268,7 @@ public Void visitSourceLocation(ISourceLocation o) throws IOException { } else { if (!o.hasOffsetLength()) { - if ("file".equals(o.getScheme())) { + if (fileLocationsAsPathOnly && "file".equals(o.getScheme())) { out.value(o.getPath()); } else { diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc index 65602a9e73b..350a355e7fc 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc @@ -14,8 +14,8 @@ import Node; loc targetFile = |memory://test-tmp/test-<"">.json|; public int maxLong = floor(pow(2,63)); -bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool rationalsAsString=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false) { - json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, rationalsAsString=rationalsAsString, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes); +bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool rationalsAsString=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false, bool fileLocationsAsPathOnly=true) { + json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, rationalsAsString=rationalsAsString, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes, fileLocationsAsPathOnly=fileLocationsAsPathOnly); readBack = normalizer(parseJSON(returnType, json, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes)); if (readBack !:= normalizer(dt) /* ignores additional src fields */) { println("What is read back, a :"); @@ -59,6 +59,7 @@ test bool jsonWithNum1(num dt) = writeRead(#num, dt, normalizer=numNormalizer); test bool jsonWithLoc1(loc dt) = writeRead(#loc, dt); test bool jsonWithLoc2(loc dt) = writeRead(#loc, dt, unpackedLocations=true); +test bool jsonWithLoc3(loc dt) = writeRead(#loc, dt, fileLocationsAsPathOnly=false); test bool jsonWithStr1(str dt) = writeRead(#str, dt); test bool jsonWithDatetime1(datetime dt) = writeRead(#datetime, dt); test bool jsonWithDatetime2(datetime dt) = writeRead(#datetime, dt, dateTimeAsInt=true); diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index a98798b79ea..54d0854126a 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -286,6 +286,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -294,6 +295,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(edt != null ? ((IBool) fpo).getValue() : true) ; return new ICommandOutput() { diff --git a/src/org/rascalmpl/library/util/Webserver.java b/src/org/rascalmpl/library/util/Webserver.java index 5dd103a0b5e..e39689c5e4c 100644 --- a/src/org/rascalmpl/library/util/Webserver.java +++ b/src/org/rascalmpl/library/util/Webserver.java @@ -245,6 +245,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -253,6 +254,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(fpo != null ? ((IBool) fpo).getValue() : true) ; try { diff --git a/src/org/rascalmpl/repl/http/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java index 498517f36ba..f9add746e92 100644 --- a/src/org/rascalmpl/repl/http/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -167,6 +167,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); + IValue fpo = kws.getParameter("fileLocationsAsPathOnly"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") @@ -175,6 +176,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) + .setFileLocationsAsPathOnly(fpo != null ? ((IBool) fpo).getValue() : true); ; try { diff --git a/src/org/rascalmpl/shell/RascalShell.java b/src/org/rascalmpl/shell/RascalShell.java index f965058e152..70d23b5f820 100644 --- a/src/org/rascalmpl/shell/RascalShell.java +++ b/src/org/rascalmpl/shell/RascalShell.java @@ -14,8 +14,8 @@ package org.rascalmpl.shell; import java.io.IOException; -import java.io.Writer; import java.io.PrintWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -38,9 +38,6 @@ public static void main(String[] args) throws IOException { for (; i < args.length; i++) { if (args[i].equals("--remoteIDEServicesPort")) { ideServicesPort = Integer.parseInt(args[++i]); - } else if (args[i].equals("--vfsPort")) { - System.err.println("Ignored parameter --vfsPort and its argument"); - i++; // skip the argument } else if (args[i].startsWith("--")) { // Currently unknown named argument, skipping over this System.err.println("Ignored parameter " + args[i]); diff --git a/src/org/rascalmpl/uri/IExternalResolverRegistry.java b/src/org/rascalmpl/uri/IExternalResolverRegistry.java new file mode 100644 index 00000000000..34f233155b8 --- /dev/null +++ b/src/org/rascalmpl/uri/IExternalResolverRegistry.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri; + +public interface IExternalResolverRegistry extends ISourceLocationInputOutput, ILogicalSourceLocationResolver, ISourceLocationWatcher { + @Override + default String scheme() { + throw new UnsupportedOperationException("'scheme' is not supported for external resolvers"); + } + + @Override + default String authority() { + throw new UnsupportedOperationException("`authority` is not supported for external resolvers"); + } +} diff --git a/src/org/rascalmpl/uri/ISourceLocationInput.java b/src/org/rascalmpl/uri/ISourceLocationInput.java index 8995aa94837..4433a44297b 100644 --- a/src/org/rascalmpl/uri/ISourceLocationInput.java +++ b/src/org/rascalmpl/uri/ISourceLocationInput.java @@ -33,10 +33,10 @@ default long created(ISourceLocation uri) throws IOException { /** In bytes !only for internal use! */ long size(ISourceLocation uri) throws IOException; boolean isDirectory(ISourceLocation uri); - boolean isFile(ISourceLocation uri) ; + boolean isFile(ISourceLocation uri); boolean isReadable(ISourceLocation uri) throws IOException; - String[] list(ISourceLocation uri) throws IOException; + String[] list(ISourceLocation uri) throws IOException; String scheme(); boolean supportsHost(); default boolean supportsReadableFileChannel() { diff --git a/src/org/rascalmpl/uri/ISourceLocationWatcher.java b/src/org/rascalmpl/uri/ISourceLocationWatcher.java index 3ba04e8bbe5..c1c5aa9bdcb 100644 --- a/src/org/rascalmpl/uri/ISourceLocationWatcher.java +++ b/src/org/rascalmpl/uri/ISourceLocationWatcher.java @@ -44,7 +44,6 @@ public interface ISourceLocationWatcher { */ boolean supportsRecursiveWatch(); - public interface ISourceLocationChanged { ISourceLocation getLocation(); ISourceLocationChangeType getChangeType(); @@ -63,9 +62,9 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - CREATED(), - DELETED(), - MODIFIED() + CREATED, + DELETED, + MODIFIED; } static ISourceLocationChanged created(ISourceLocation loc) { diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index ad8facf6475..534ac7eb5f3 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -44,6 +44,7 @@ import org.rascalmpl.unicode.UnicodeOutputStreamWriter; import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChanged; import org.rascalmpl.uri.classloaders.IClassloaderLocationResolver; +import org.rascalmpl.uri.remote.RemoteExternalResolverRegistry; import org.rascalmpl.uri.watch.WatchRegistry; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; @@ -65,12 +66,7 @@ public class URIResolverRegistry { private final Map> logicalResolvers = new ConcurrentHashMap<>(); private final Map classloaderResolvers = new ConcurrentHashMap<>(); - // we allow the user to define (using -Drascal.fallbackResolver=fully.qualified.classname) a single class that will handle - // scheme's not statically registered. That class should implement at least one of these interfaces - private volatile @Nullable ISourceLocationInput fallbackInputResolver; - private volatile @Nullable ISourceLocationOutput fallbackOutputResolver; - private volatile @Nullable ILogicalSourceLocationResolver fallbackLogicalResolver; - private volatile @Nullable IClassloaderLocationResolver fallbackClassloaderResolver; + private volatile @Nullable IExternalResolverRegistry externalRegistry; private static class InstanceHolder { static URIResolverRegistry sInstance = new URIResolverRegistry(); @@ -116,14 +112,34 @@ private void loadServices() { try { Enumeration resources = getClass().getClassLoader().getResources(RESOLVERS_CONFIG); Collections.list(resources).forEach(f -> loadServices(f)); - var fallbackResolverClassName = System.getProperty("rascal.fallbackResolver"); - if (fallbackResolverClassName != null) { - loadFallback(fallbackResolverClassName); - } } catch (IOException e) { throw new Error("WARNING: Could not load URIResolverRegistry extensions from " + RESOLVERS_CONFIG, e); } + + var remoteResolverRegistryPort = getRemoteResolverRegistryPort(); + if (remoteResolverRegistryPort != null) { + registerRemoteResolverRegistry(new RemoteExternalResolverRegistry(remoteResolverRegistryPort)); + } + } + + public static Integer getRemoteResolverRegistryPort() { + var remoteResolverRegistryPortProperty = System.getProperty("rascal.remoteResolverRegistryPort"); + if (remoteResolverRegistryPortProperty != null) { + try { + return Integer.parseInt(remoteResolverRegistryPortProperty); + } catch (NumberFormatException e) { + System.err.println("WARNING: Invalid remoteResolverRegistryPort environment variable: " + remoteResolverRegistryPortProperty + " is not parseable as integer"); + } + } + return null; + } + + public synchronized void registerRemoteResolverRegistry(RemoteExternalResolverRegistry registry) { + if (this.externalRegistry != null) { + this.externalRegistry = registry; + watchers.setExternalRegistry(registry); + } } public Set getRegisteredInputSchemes() { @@ -153,46 +169,6 @@ private Object constructService(String name) throws ClassNotFoundException, Inst } } - private void loadFallback(String fallbackClass) { - try { - Object instance = constructService(fallbackClass); - boolean ok = false; - if (instance instanceof ILogicalSourceLocationResolver) { - fallbackLogicalResolver = (ILogicalSourceLocationResolver) instance; - ok = true; - } - - if (instance instanceof ISourceLocationInput) { - fallbackInputResolver = (ISourceLocationInput) instance; - ok = true; - } - - if (instance instanceof ISourceLocationOutput) { - fallbackOutputResolver = (ISourceLocationOutput) instance; - ok = true; - } - - if (instance instanceof IClassloaderLocationResolver) { - fallbackClassloaderResolver = (IClassloaderLocationResolver) instance; - ok = true; - } - - if (instance instanceof ISourceLocationWatcher) { - watchers.setFallback((ISourceLocationWatcher) instance); - } - if (!ok) { - System.err.println("WARNING: could not load fallback resolver " + fallbackClass - + " because it does not implement ISourceLocationInput or ISourceLocationOutput or ILogicalSourceLocationResolver"); - } - } - catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException - | IllegalArgumentException | InvocationTargetException | SecurityException e) { - System.err.println("WARNING: could not load resolver due to " + e.getMessage()); - e.printStackTrace(); - } - - } - private void loadServices(URL nextElement) { try { for (String name : readConfigFile(nextElement)) { @@ -402,9 +378,9 @@ private ISourceLocation physicalLocation(ISourceLocation loc) throws IOException loc = resolveAndFixOffsets(loc, resolver, map.values()); } - if (fallbackLogicalResolver != null) { - var fallbackResult = resolveAndFixOffsets(loc == null ? original : loc, fallbackLogicalResolver, Collections.emptyList()); - return fallbackResult == null ? loc : fallbackResult; + if (externalRegistry != null) { + var externalResult = resolveAndFixOffsets(loc == null ? original : loc, externalRegistry, Collections.emptyList()); + return externalResult == null ? loc : externalResult; } return loc; } @@ -464,7 +440,7 @@ private ISourceLocationInput getInputResolver(String scheme) { return result; } } - return fallbackInputResolver; + return externalRegistry; } return result; } @@ -480,7 +456,6 @@ private IClassloaderLocationResolver getClassloaderResolver(String scheme) { return result; } } - return fallbackClassloaderResolver; } return result; } @@ -496,7 +471,7 @@ private ISourceLocationOutput getOutputResolver(String scheme) { return result; } } - return fallbackOutputResolver; + return externalRegistry; } return result; } @@ -1118,7 +1093,7 @@ public boolean hasLogicalResolver(ISourceLocation loc) { } public boolean hasNativelyWatchableResolver(ISourceLocation loc) { - return watchers.hasNativeSupport(loc.getScheme()) || watchers.hasNativeSupport(safeResolve(loc).getScheme()) || watchers.hasFallback(); + return watchers.hasNativeSupport(loc.getScheme()) || watchers.hasNativeSupport(safeResolve(loc).getScheme()) || watchers.hasExternalRegistry(); } public FileAttributes stat(ISourceLocation loc) throws IOException { diff --git a/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java new file mode 100644 index 00000000000..04b7d95ff52 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote; + +import java.io.IOException; +import java.nio.file.NotDirectoryException; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; +import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChangeType; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; +import org.rascalmpl.uri.remote.jsonrpc.NumberResponse; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +import org.rascalmpl.uri.remote.jsonrpc.TimestampResponse; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; +import org.rascalmpl.util.NamedThreadPool; +import org.rascalmpl.util.base64.StreamingBase64; + +import io.usethesource.vallang.ISourceLocation; + +/** + * RascalFileSystemServices offers remote access to the Rascal file system. + * Currently, this is limited to a single client-server connection. + */ +public class RascalFileSystemServices implements IRemoteResolverRegistryServer { + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private static final ExecutorService executor = NamedThreadPool.cachedDaemon("rascal-vfs"); + + private volatile @MonotonicNonNull IRemoteResolverRegistryClient client = null; + + @EnsuresNonNull("this.client") + protected void provideClient(IRemoteResolverRegistryClient client) { + this.client = client; + } + + @FunctionalInterface + private interface IOSupplier { + T supply() throws IOException; + } + + @FunctionalInterface + private interface IORunner { + void run() throws IOException; + } + + private CompletableFuture async(IORunner job) { + return CompletableFuture.runAsync(() -> { + try { + job.run(); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + private CompletableFuture async(IOSupplier job) { + return CompletableFuture.supplyAsync(() -> { + try { + return job.supply(); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture readFile(ISourceLocationRequest req) { + return async(() -> { + StringBuilder builder = new StringBuilder(); + StreamingBase64.encode(reg.getInputStream(req.getLocation()), builder, true); + return new LocationContentResponse(builder.toString()); + }); + } + + @Override + public CompletableFuture exists(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.exists(req.getLocation()))); + } + + @Override + public CompletableFuture lastModified(ISourceLocationRequest req) { + return async(() -> new TimestampResponse(reg.lastModified(req.getLocation()))); + } + + @Override + public CompletableFuture created(ISourceLocationRequest req) { + return async(() -> new TimestampResponse(reg.created(req.getLocation()))); + } + + @Override + public CompletableFuture isDirectory(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isDirectory(req.getLocation()))); + } + + @Override + public CompletableFuture isFile(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isFile(req.getLocation()))); + } + + @Override + public CompletableFuture list(ISourceLocationRequest req) { + return async(() -> { + ISourceLocation loc = req.getLocation(); + if (!reg.isDirectory(loc)) { + throw new NotDirectoryException(loc.toString()); + } + return Arrays.stream(reg.list(loc)) + .map(l -> new FileWithType(URIUtil.getLocationName(l), reg.isDirectory(l) ? FileType.Directory : FileType.File)) + .toArray(FileWithType[]::new); + }); + } + + @Override + public CompletableFuture size(ISourceLocationRequest req) { + return async(() -> new NumberResponse(reg.size(req.getLocation()))); + } + + @Override + public CompletableFuture stat(ISourceLocationRequest req) { + return async(() -> reg.stat(req.getLocation())); + } + + @Override + public CompletableFuture isReadable(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isReadable(req.getLocation()))); + } + + @Override + public CompletableFuture setLastModified(SetLastModifiedRequest req) { + return async(() -> reg.setLastModified(req.getLocation(), req.getTimestamp())); + } + + @Override + public CompletableFuture isWritable(ISourceLocationRequest req) { + return async(() -> new BooleanResponse(reg.isWritable(req.getLocation()))); + } + + @Override + public CompletableFuture writeFile(WriteFileRequest req) { + return async(() -> { + try (var decoder = StreamingBase64.decode(req.getContent()); + var target = reg.getOutputStream(req.getLocation(), req.isAppend())) { + decoder.transferTo(target); + } + }); + } + + @Override + public CompletableFuture mkDirectory(ISourceLocationRequest req) { + return async(() -> reg.mkDirectory(req.getLocation())); + } + + @Override + public CompletableFuture remove(RemoveRequest req) { + return async(() -> reg.remove(req.getLocation(), req.isRecursive())); + } + + @Override + public CompletableFuture rename(RenameRequest req) { + return async(() -> reg.rename(req.getFrom(), req.getTo(), req.isOverwrite())); + } + + @Override + public CompletableFuture copy(CopyRequest req) { + return async(() -> reg.copy(req.getFrom(), req.getTo(), req.isRecursive(), req.isOverwrite())); + } + + @Override + public CompletableFuture watch(WatchRequest params) { + return async(() -> { + URIResolverRegistry.getInstance().watch(params.getLocation(), params.isRecursive(), changed -> + client.sourceLocationChanged(new ISourceLocationChanged( + changed.getLocation(), ISourceLocationChangeType.translate(changed.getChangeType()), params.getWatchId() + )) + ); + }); + } + + @Override + public CompletableFuture unwatch(WatchRequest req) { + return async(() -> URIResolverRegistry.getInstance().unwatch(req.getLocation(), req.isRecursive(), e -> {})); + } + + @Override + public CompletableFuture supportsRecursiveWatch() { + return async(() -> new BooleanResponse(true)); + } + + @Override + public CompletableFuture resolveLocation(ISourceLocationRequest req) { + return async(() -> { + ISourceLocation loc = req.getLocation(); + ISourceLocation resolved = reg.logicalToPhysical(loc); + + if (resolved == null) { + resolved = loc; + } + + return new SourceLocationResponse(resolved); + }); + } +} diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java new file mode 100644 index 00000000000..a979db563b5 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -0,0 +1,617 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.nio.file.AccessDeniedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.IExternalResolverRegistry; +import org.rascalmpl.uri.ISourceLocationWatcher; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryClient; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileType; +import org.rascalmpl.uri.vfs.IRemoteResolverRegistryServer.FileWithType; +import org.rascalmpl.util.Lazy; +import org.rascalmpl.util.NamedThreadPool; +import org.rascalmpl.util.base64.StreamingBase64; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.gson.JsonPrimitive; + +import io.usethesource.vallang.ISourceLocation; + +/** + * Default implementation for access to a remote file system. + */ +public class RemoteExternalResolverRegistry implements IExternalResolverRegistry, IRemoteResolverRegistryClient { + private volatile IRemoteResolverRegistryServer remote = null; + private static final ExecutorService exec = NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry"); + + private final Map watchers = new ConcurrentHashMap<>(); + private final Map watchersById = new ConcurrentHashMap<>(); + + private final int remoteResolverRegistryPort; + + public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { + this.remoteResolverRegistryPort = remoteResolverRegistryPort; + scheduleReconnect(); + } + + private static final Duration LONGEST_TIMEOUT = Duration.ofMinutes(1); + + private void connect() { + var timeout = Duration.ZERO; + while (true) { + try { + Thread.sleep(timeout.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + var remote = startClient(); + if (remote != null) { + this.remote = remote; + return; + } + timeout = timeout.plusMillis(10); + if (timeout.compareTo(LONGEST_TIMEOUT) >= 0) { + timeout = LONGEST_TIMEOUT; + } + } + } + + private void scheduleReconnect() { + CompletableFuture.runAsync(() -> connect()); + } + + @FunctionalInterface + private interface ThrowingSupplier { + T get() throws E; + } + + @FunctionalInterface + private interface ThrowingFunction { + R apply(T t) throws E; + } + + @FunctionalInterface + private interface ThrowingTriFunction { + R apply(T t, U u, V v) throws E; + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws E; + } + + @FunctionalInterface + private interface ThrowingConsumer { + void accept(T t) throws E; + } + + @FunctionalInterface + private interface ThrowingTriConsumer { + void accept(T t, U u, V v) throws E; + } + + private InputStream errorDetectingInputStream(InputStream original) { + return new InputStream() { + private T socketExceptionCatcher(ThrowingSupplier function) throws IOException { + try { + return function.get(); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + private R socketExceptionCatcher(ThrowingFunction function, T t) throws IOException { + try { + return function.apply(t); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + private R socketExceptionCatcher(ThrowingTriFunction function, T t, U u, V v) throws IOException { + try { + return function.apply(t, u, v); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public int read() throws IOException { + return socketExceptionCatcher(original::read); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return socketExceptionCatcher(original::read, b, off, len); + } + + @Override + public int available() throws IOException { + return socketExceptionCatcher(original::available); + } + + @Override + public long skip(long n) throws IOException { + return socketExceptionCatcher(original::skip, n); + } + + @Override + public void close() throws IOException { + original.close(); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return socketExceptionCatcher(original::readNBytes, len); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return socketExceptionCatcher(original::readNBytes, b, off, len); + } + }; + } + + private OutputStream errorDetectingOutputStream(OutputStream original) { + return new OutputStream() { + private void socketExceptionCatcher(ThrowingRunnable runnable) throws IOException { + try { + runnable.run(); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + private void socketExceptionCatcher(ThrowingConsumer consumer, T t) throws IOException { + try { + consumer.accept(t); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + private void socketExceptionCatcher(ThrowingTriConsumer consumer, T t, U u, V v) throws IOException { + try { + consumer.accept(t, u, v); + } catch (SocketException e) { + scheduleReconnect(); + throw e; + } + } + + @Override + public void write(int b) throws IOException { + socketExceptionCatcher(original::write, b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + socketExceptionCatcher(original::write, b, off, len); + } + + @Override + public void flush() throws IOException { + socketExceptionCatcher(original::flush); + } + + @Override + public void close() throws IOException { + original.close(); + } + }; + } + + private IRemoteResolverRegistryServer startClient() { + try { + @SuppressWarnings("resource") + var socket = new Socket(InetAddress.getLoopbackAddress(), remoteResolverRegistryPort); + socket.setTcpNoDelay(true); + Launcher clientLauncher = new Launcher.Builder() + .setRemoteInterface(IRemoteResolverRegistryServer.class) + .setLocalService(this) + .setInput(errorDetectingInputStream(socket.getInputStream())) + .setOutput(errorDetectingOutputStream(socket.getOutputStream())) + .configureGson(GsonUtils.complexAsJsonObject()) + .setExecutorService(exec) + .create(); + + clientLauncher.startListening(); + return clientLauncher.getRemoteProxy(); + } catch (RuntimeException | IOException e) { + System.err.println("Error setting up remote resolver registry connection, will reconnect: " + e.getMessage()); + return null; + } + } + + private static U call(Function> function, T argument) throws IOException { + try { + return function.apply(argument).get(1, TimeUnit.MINUTES); + } catch (TimeoutException e) { + throw new IOException("Remote resolver took too long to reply; interrupted to avoid deadlocks"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnsupportedOperationException("Thread should have been interrupted"); + } catch (CompletionException | ExecutionException e) { + var cause = e.getCause(); + if (cause instanceof ResponseErrorException) { + throw translateException((ResponseErrorException) cause); + } + throw new IOException(e); + } + } + + private static final int JsonRpcErrorCode_Generic = -1; + private static final int JsonRpcErrorCode_FileSystem = -2; + private static final int JsonRpcErrorCode_NativeRascal = -3; + + private static IOException translateException(ResponseErrorException cause) { + var error = cause.getResponseError(); + switch (error.getCode()) { + case JsonRpcErrorCode_Generic: + return new IOException("Generic error: " + error.getMessage()); + case JsonRpcErrorCode_FileSystem: { + if (error.getData() instanceof JsonPrimitive) { + var data = (JsonPrimitive) error.getData(); + if (data.isString()) { + switch (data.getAsString()) { + case "FileExists": // fall-through + case "EntryExists": + return new FileAlreadyExistsException(error.getMessage()); + case "FileNotFound": // fall-through + case "EntryNotFound": + return new NoSuchFileException(error.getMessage()); + case "FileNotADirectory": // fall-through + case "EntryNotADirectory": + return new NotDirectoryException(error.getMessage()); + case "FileIsADirectory": // fall-through + case "EntryIsADirectory": + return new IOException("File is a directory: " + error.getMessage()); + case "NoPermissions": + return new AccessDeniedException(error.getMessage()); + } + } + } + return new IOException("File system error: " + error.getMessage() + " data: " + error.getData()); + } + case JsonRpcErrorCode_NativeRascal: + return new IOException("Rascal native schemes should not be forwarded"); + default: + return new IOException("Missing case for: " + error); + } + } + + @Override + public InputStream getInputStream(ISourceLocation loc) throws IOException { + return StreamingBase64.decode(call(remote::readFile, new ISourceLocationRequest(loc)).getContent()); + } + + @Override + public boolean exists(ISourceLocation loc) { + try { + return call(remote::exists, new ISourceLocationRequest(loc)).getValue(); + } catch (IOException e) { + return false; + } + } + + @Override + public long lastModified(ISourceLocation loc) throws IOException { + return call(remote::lastModified, new ISourceLocationRequest(loc)).getTimestamp(); + } + + @Override + public long size(ISourceLocation loc) throws IOException { + return call(remote::size, new ISourceLocationRequest(loc)).getNumber(); + } + + @Override + public boolean isDirectory(ISourceLocation loc) { + try { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + return result != null && result; + } + return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); + } catch (IOException e) { + return false; + } + } + + @Override + public boolean isFile(ISourceLocation loc) { + try { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + return result != null && !result; + } + return call(remote::isFile, new ISourceLocationRequest(loc)).getValue(); + } catch (IOException e) { + return false; + } + } + + @Override + public boolean isReadable(ISourceLocation loc) throws IOException { + return call(remote::isReadable, new ISourceLocationRequest(loc)).getValue(); + } + + /** + * Rascal's current implementions sometimes ask for a directory listing and then iterate over all entries + * checking whether they are a directory. This is very slow for JSON-RPC, so we store the last directory listing + * and check the cache first + */ + private final Cache>> cachedDirectoryListing + = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofSeconds(5)) + .maximumSize(1000) + .build(); + + @Override + public String[] list(ISourceLocation loc) throws IOException { + var result = call(remote::list, new ISourceLocationRequest(loc)); + cachedDirectoryListing.put(loc, Lazy.defer(() -> { + return Stream.of(result).collect(Collectors.toMap(FileWithType::getName, e -> e.getType() == FileType.Directory)); + })); + return Stream.of(result).map(FileWithType::getName).toArray(String[]::new); + } + + @Override + public boolean supportsHost() { + return false; + } + + @Override + public FileAttributes stat(ISourceLocation loc) throws IOException { + return call(remote::stat, new ISourceLocationRequest(loc)); + } + + @Override + public OutputStream getOutputStream(ISourceLocation loc, boolean append) throws IOException { + var content = new StringBuilder(); + return StreamingBase64.encode(content, () -> { + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + try { + call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); + } catch (IOException e) { + // Ignore + } + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + }); + } + + @Override + public void mkDirectory(ISourceLocation loc) throws IOException { + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + call(remote::mkDirectory, new ISourceLocationRequest(loc)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } + + @Override + public void remove(ISourceLocation loc) throws IOException { + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + call(remote::remove, new RemoveRequest(loc, true)); + cachedDirectoryListing.invalidate(loc); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } + + @Override + public void setLastModified(ISourceLocation loc, long timestamp) throws IOException { + call(remote::setLastModified, new SetLastModifiedRequest(loc, timestamp)); + } + + @Override + public boolean isWritable(ISourceLocation loc) throws IOException { + return call(remote::isWritable, new ISourceLocationRequest(loc)).getValue(); + } + + @Override + public ISourceLocation resolve(ISourceLocation input) throws IOException { + return call(remote::resolveLocation, new ISourceLocationRequest(input)).getLocation(); + } + + @Override + public void watch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + synchronized (watchers) { + var key = new WatchSubscriptionKey(root, recursive); + var watch = watchers.get(key); + if (watch == null) { + watch = new Watchers(); + watch.addNewWatcher(watcher); + watchersById.put(watch.getId(), watch); + call(remote::watch, new WatchRequest(root, recursive, watch.getId())); + watchers.put(key, watch); + } + watch.addNewWatcher(watcher); + } + } + + @Override + public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + synchronized (watchers) { + var watchKey = new WatchSubscriptionKey(root, recursive); + var watch = watchers.get(watchKey); + if (watch != null && watch.removeWatcher(watcher)) { + watchers.remove(watchKey); + if (!watch.getCallbacks().isEmpty()) { + watchers.put(watchKey, watch); + return; + } + watchersById.remove(watch.getId()); + call(remote::unwatch, new WatchRequest(root, recursive, watch.getId())); + } + } + } + + @Override + public boolean supportsRecursiveWatch() { + try { + return call(n -> remote.supportsRecursiveWatch(), null).getValue(); + } catch (IOException e) { + return false; + } + } + + @Override + public void sourceLocationChanged(org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged changed) { + var watcher = watchersById.get(changed.getWatchId()); + var root = changed.getRoot(); + if (watcher == null) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.RequestFailed, "Received notification for unregistered watch", root)); + } + try { + switch (changed.getChangeType()) { + case CREATED: + watcher.publish(ISourceLocationWatcher.created(root)); + break; + case DELETED: + watcher.publish(ISourceLocationWatcher.deleted(root)); + break; + case MODIFIED: + watcher.publish(ISourceLocationWatcher.modified(root)); + break; + } + } catch (IllegalArgumentException e) { + throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Unexpected FileChangeType " + changed.getChangeType().getValue(), root)); + } + } + + /** + * The watch API in Rascal uses closures identity to keep track of watches. Since we cannot share the instance + * via the JSON-RPC bridge, we keep the closure around in this collection class. + * If there are no more callbacks registered, we unregister the watch at the remote side. + */ + private static class Watchers { + private final String id; + private final List> callbacks = new CopyOnWriteArrayList<>(); + + public Watchers() { + this.id = UUID.randomUUID().toString(); + } + + public void addNewWatcher(Consumer watcher) { + this.callbacks.add(watcher); + } + + public boolean removeWatcher(Consumer watcher) { + this.callbacks.remove(watcher); + return this.callbacks.isEmpty(); + } + + public void publish(ISourceLocationWatcher.ISourceLocationChanged changed) { + for (Consumer c : callbacks) { + //schedule callbacks on different thread + exec.submit(() -> c.accept(changed)); + } + } + + public String getId() { + return id; + } + + public List> getCallbacks() { + return callbacks; + } + } + + public static class WatchSubscriptionKey { + private final ISourceLocation loc; + private final boolean recursive; + public WatchSubscriptionKey(ISourceLocation loc, boolean recursive) { + this.loc = loc; + this.recursive = recursive; + } + + @Override + public int hashCode() { + return Objects.hash(loc, recursive); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj instanceof WatchSubscriptionKey)) { + WatchSubscriptionKey other = (WatchSubscriptionKey) obj; + return recursive == other.recursive + && Objects.equals(loc, other.loc) + ; + } + return false; + } + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java new file mode 100644 index 00000000000..1cc74712556 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +public class BooleanResponse { + private final boolean value; + + public BooleanResponse(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BooleanResponse) { + return value == ((BooleanResponse)obj).value; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java new file mode 100644 index 00000000000..6a0fb695c37 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/CopyRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class CopyRequest { + private final ISourceLocation from; + private final ISourceLocation to; + private final boolean recursive; + private final boolean overwrite; + + public CopyRequest(ISourceLocation from, ISourceLocation to, boolean recursive, boolean overwrite) { + this.from = from; + this.to = to; + this.recursive = recursive; + this.overwrite = overwrite; + } + + public ISourceLocation getFrom() { + return from; + } + + public ISourceLocation getTo() { + return to; + } + + public boolean isRecursive() { + return recursive; + } + + public boolean isOverwrite() { + return overwrite; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CopyRequest) { + var other = (CopyRequest)obj; + return Objects.equals(from, other.from) + && Objects.equals(to, other.to) + && recursive == other.recursive + && overwrite == other.overwrite + ; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(from, to, recursive, overwrite); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java new file mode 100644 index 00000000000..83bad0aa032 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import org.rascalmpl.uri.ISourceLocationWatcher; + +/** + * @see ISourceLocationWatcher.ISourceLocationChangeType this code is mirroring this type for serialization purposes + */ +public enum ISourceLocationChangeType { + CREATED(1), + DELETED(2), + MODIFIED(3); + + private final int value; + + ISourceLocationChangeType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static ISourceLocationChangeType forValue(int value) { + var allValues = ISourceLocationChangeType.values(); + if (value < 1 || value > allValues.length) { + throw new IllegalArgumentException("Illegal enum value: " + value); + } + return allValues[value - 1]; + } + + public static ISourceLocationWatcher.ISourceLocationChangeType translate(ISourceLocationChangeType lsp) { + switch (lsp) { + case CREATED: + return ISourceLocationWatcher.ISourceLocationChangeType.CREATED; + case DELETED: + return ISourceLocationWatcher.ISourceLocationChangeType.DELETED; + case MODIFIED: + return ISourceLocationWatcher.ISourceLocationChangeType.MODIFIED; + default: + throw new IllegalArgumentException("Unknown ISourceLocationChangeType " + lsp); + } + } + + public static ISourceLocationChangeType translate(ISourceLocationWatcher.ISourceLocationChangeType rascal) { + switch (rascal) { + case CREATED: + return ISourceLocationChangeType.CREATED; + case DELETED: + return ISourceLocationChangeType.DELETED; + case MODIFIED: + return ISourceLocationChangeType.MODIFIED; + default: + throw new IllegalArgumentException("Unknown ISourceLocationChangeType " + rascal); + } + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java new file mode 100644 index 00000000000..fe2723cd934 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import org.rascalmpl.uri.ISourceLocationWatcher; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationChanged { + private final ISourceLocation root; + private final ISourceLocationChangeType type; + private final String watchId; + + public ISourceLocationChanged(ISourceLocation root, ISourceLocationChangeType type, String watchId) { + this.root = root; + this.type = type; + this.watchId = watchId; + } + + public ISourceLocation getRoot() { + return root; + } + + public ISourceLocationChangeType getChangeType() { + return type; + } + + public String getWatchId() { + return watchId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ISourceLocationChanged) { + var other = (ISourceLocationChanged)obj; + return Objects.equals(root, other.root) + && Objects.equals(type, other.type) + && Objects.equals(watchId, other.watchId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(root, type, watchId); + } + + public ISourceLocationWatcher.ISourceLocationChanged translate() { + return ISourceLocationWatcher.makeChange(getRoot(), ISourceLocationChangeType.translate(type)); + } + + @Override + public String toString() { + return "ISourceLocationChanged [changeType=" + type + ", root=" + root + ", watchId=" + watchId + "]"; + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java new file mode 100644 index 00000000000..ef5712c3654 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationRequest { + private final ISourceLocation loc; + + public ISourceLocationRequest(ISourceLocation loc) { + this.loc = loc; + } + + public ISourceLocation getLocation() { + return loc; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ISourceLocationRequest) { + return loc.equals(((ISourceLocationRequest)obj).loc); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(loc.hashCode()); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java new file mode 100644 index 00000000000..3aa98bfdac9 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +public class LocationContentResponse { + private final String content; + + public LocationContentResponse(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LocationContentResponse) { + return content.equals(((LocationContentResponse)obj).content); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(content); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java new file mode 100644 index 00000000000..5569b3f0245 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +public class NumberResponse { + private final long number; + + public NumberResponse(long number) { + this.number = number; + } + + public long getNumber() { + return number; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NumberResponse) { + return number == ((NumberResponse)obj).number; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(number); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java new file mode 100644 index 00000000000..d32a65ad7b3 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class RemoveRequest extends ISourceLocationRequest { + private final boolean recursive; + + public RemoveRequest(ISourceLocation loc, boolean recursive) { + super(loc); + this.recursive = recursive; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof RemoveRequest) { + var other = (RemoveRequest)obj; + return super.equals(obj) + && recursive == other.recursive; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), recursive); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java new file mode 100644 index 00000000000..c37c81b16f6 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class RenameRequest { + private final ISourceLocation from; + private final ISourceLocation to; + private final boolean overwrite; + + public RenameRequest(ISourceLocation from, ISourceLocation to, boolean overwrite) { + this.from = from; + this.to = to; + this.overwrite = overwrite; + } + + public ISourceLocation getFrom() { + return from; + } + + public ISourceLocation getTo() { + return to; + } + + public boolean isOverwrite() { + return overwrite; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof RenameRequest) { + var other = (RenameRequest)obj; + return Objects.equals(from, other.from) + && Objects.equals(to, other.to) + && overwrite == other.overwrite + ; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(from, to, overwrite); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java new file mode 100644 index 00000000000..142b2798fe5 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class SetLastModifiedRequest extends ISourceLocationRequest{ + private final long timestamp; + + public SetLastModifiedRequest(ISourceLocation loc, long timestamp) { + super(loc); + this.timestamp = timestamp; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SetLastModifiedRequest) { + var other = (SetLastModifiedRequest)obj; + return super.equals(other) && timestamp == other.timestamp; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), timestamp); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java new file mode 100644 index 00000000000..a4a509965d1 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class SourceLocationResponse { + private final ISourceLocation loc; + + public SourceLocationResponse(ISourceLocation loc) { + this.loc = loc; + } + + public ISourceLocation getLocation() { + return loc; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SourceLocationResponse) { + return loc.equals(((SourceLocationResponse)obj).loc); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(loc); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java new file mode 100644 index 00000000000..b3f88e7bdff --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +public class TimestampResponse { + private final long timestamp; + + public TimestampResponse(long timestamp) { + this.timestamp = timestamp; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TimestampResponse) { + return timestamp == ((TimestampResponse)obj).timestamp; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(timestamp); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java new file mode 100644 index 00000000000..eba75913ab1 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class WatchRequest extends ISourceLocationRequest { + private final String watchId; + private final boolean recursive; + + public WatchRequest(ISourceLocation loc, boolean recursive, String watchId) { + super(loc); + this.recursive = recursive; + this.watchId = watchId; + } + + public String getWatchId() { + return watchId; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WatchRequest) { + var other = (WatchRequest)obj; + return super.equals(other) + && other.recursive == recursive + && Objects.equals(watchId, other.watchId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), watchId, recursive); + } +} diff --git a/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java new file mode 100644 index 00000000000..e87d46cc8a7 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.remote.jsonrpc; + +import java.util.Objects; + +import io.usethesource.vallang.ISourceLocation; + +public class WriteFileRequest extends ISourceLocationRequest { + private final String content; + private final boolean append; + + public WriteFileRequest(ISourceLocation loc, String content, boolean append) { + super(loc); + this.content = content; + this.append = append; + } + + public String getContent() { + return content; + } + + public boolean isAppend() { + return append; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WriteFileRequest) { + var other = (WriteFileRequest)obj; + return super.equals(obj) + && content.equals(other.content) + && append == other.append; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), content, append); + } +} diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java new file mode 100644 index 00000000000..ea75d4b6ee2 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.vfs; + +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationChanged; + +/** + * This interface defines the JSON-RPC interface for callbacks related to remote file systems. + */ +@JsonSegment("rascal/vfs/watcher") +public interface IRemoteResolverRegistryClient { + @JsonNotification + void sourceLocationChanged(ISourceLocationChanged changed); +} diff --git a/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java new file mode 100644 index 00000000000..1604a00cc22 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri.vfs; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; +import org.rascalmpl.uri.FileAttributes; +import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse; +import org.rascalmpl.uri.remote.jsonrpc.CopyRequest; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; +import org.rascalmpl.uri.remote.jsonrpc.LocationContentResponse; +import org.rascalmpl.uri.remote.jsonrpc.NumberResponse; +import org.rascalmpl.uri.remote.jsonrpc.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SetLastModifiedRequest; +import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +import org.rascalmpl.uri.remote.jsonrpc.TimestampResponse; +import org.rascalmpl.uri.remote.jsonrpc.WatchRequest; +import org.rascalmpl.uri.remote.jsonrpc.WriteFileRequest; + +/** + * This interface defines the JSON-RPC interface for remote access to the Rascal file system. + */ +@JsonSegment("rascal/vfs") +public interface IRemoteResolverRegistryServer { + @JsonRequest("input/readFile") + CompletableFuture readFile(ISourceLocationRequest req); + + @JsonRequest("input/exists") + CompletableFuture exists(ISourceLocationRequest req); + + @JsonRequest("input/lastModified") + CompletableFuture lastModified(ISourceLocationRequest req); + + @JsonRequest("input/created") + CompletableFuture created(ISourceLocationRequest req); + + @JsonRequest("input/isDirectory") + CompletableFuture isDirectory(ISourceLocationRequest req); + + @JsonRequest("input/isFile") + CompletableFuture isFile(ISourceLocationRequest req); + + @JsonRequest("input/list") + CompletableFuture list(ISourceLocationRequest req); + + @JsonRequest("input/size") + CompletableFuture size(ISourceLocationRequest req); + + @JsonRequest("input/stat") + CompletableFuture stat(ISourceLocationRequest req); + + @JsonRequest("input/isReadable") + CompletableFuture isReadable(ISourceLocationRequest req); + + @JsonRequest("output/setLastModified") + CompletableFuture setLastModified(SetLastModifiedRequest req); + + @JsonRequest("output/isWritable") + CompletableFuture isWritable(ISourceLocationRequest req); + + @JsonRequest("output/writeFile") + CompletableFuture writeFile(WriteFileRequest req); + + @JsonRequest("output/mkDirectory") + CompletableFuture mkDirectory(ISourceLocationRequest req); + + @JsonRequest("output/remove") + CompletableFuture remove(RemoveRequest req); + + @JsonRequest("output/rename") + CompletableFuture rename(RenameRequest req); + + @JsonRequest("output/copy") + CompletableFuture copy(CopyRequest req); + + @JsonRequest("watcher/watch") + CompletableFuture watch(WatchRequest req); + + @JsonRequest("watcher/unwatch") + CompletableFuture unwatch(WatchRequest req); + + @JsonRequest("watcher/supportsRecursiveWatch") + CompletableFuture supportsRecursiveWatch(); + + @JsonRequest("logical/resolveLocation") + CompletableFuture resolveLocation(ISourceLocationRequest loc); + + public static class FileWithType { + @NonNull private final String name; + @NonNull private final FileType type; + + public FileWithType(@NonNull String name, @NonNull FileType type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public FileType getType() { + return type; + } + } + + public enum FileType { + Unknown(0), File(1), Directory(2), SymbolicLink(64); + + private final int value; + + private FileType(int val) { + assert val == 0 || val == 1 || val == 2 || val == 64; + this.value = val; + } + + public int getValue() { + return value; + } + } +} diff --git a/src/org/rascalmpl/uri/watch/WatchRegistry.java b/src/org/rascalmpl/uri/watch/WatchRegistry.java index 1c4251376e3..f3be99d176a 100644 --- a/src/org/rascalmpl/uri/watch/WatchRegistry.java +++ b/src/org/rascalmpl/uri/watch/WatchRegistry.java @@ -73,7 +73,7 @@ public class WatchRegistry { private final URIResolverRegistry reg; public final ReferenceQueue> clearedReferences = new ReferenceQueue<>(); private final UnaryOperator resolver; - private volatile @Nullable ISourceLocationWatcher fallback; + private volatile @Nullable ISourceLocationWatcher externalRegistry; public WatchRegistry(URIResolverRegistry reg, UnaryOperator resolver) { this.reg = reg; @@ -85,12 +85,12 @@ public WatchRegistry(URIResolverRegistry reg, UnaryOperator res public void registerNative(String scheme, ISourceLocationWatcher watcher) { watchers.put(scheme, watcher); } - public void setFallback(ISourceLocationWatcher fallback) { - this.fallback = fallback; + public void setExternalRegistry(ISourceLocationWatcher externalRegistry) { + this.externalRegistry = externalRegistry; } - public boolean hasFallback() { - return fallback != null; + public boolean hasExternalRegistry() { + return externalRegistry != null; } private ISourceLocation safeResolve(ISourceLocation loc) { @@ -122,8 +122,8 @@ public void watch(ISourceLocation loc, boolean recursive, Predicate p.isRecursive() == recursive && p.getHandler() == finalCallback); } } - else if (fallback != null) { - fallback.unwatch(loc, callback, recursive); + else if (externalRegistry != null) { + externalRegistry.unwatch(loc, callback, recursive); } } diff --git a/src/org/rascalmpl/util/Lazy.java b/src/org/rascalmpl/util/Lazy.java new file mode 100644 index 00000000000..21633b893f8 --- /dev/null +++ b/src/org/rascalmpl/util/Lazy.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.util; + +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; + +public interface Lazy extends Supplier { + public static Lazy defer(Supplier generator) { + return new Lazy(){ + private volatile @MonotonicNonNull T result = null; + + @Override + public T get() { + if (result == null) { + result = generator.get(); + } + return result; + } + }; + } +} diff --git a/src/org/rascalmpl/util/NamedThreadPool.java b/src/org/rascalmpl/util/NamedThreadPool.java new file mode 100644 index 00000000000..f0036691304 --- /dev/null +++ b/src/org/rascalmpl/util/NamedThreadPool.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.util; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class NamedThreadPool { + private NamedThreadPool() {} + + public static ExecutorService single(String name) { + return Executors.newSingleThreadExecutor(factory(name, false)); + } + + public static ExecutorService singleDaemon(String name) { + return Executors.newSingleThreadExecutor(factory(name, true)); + } + + public static ExecutorService cached(String name) { + return Executors.newCachedThreadPool(factory(name, false)); + } + + public static ExecutorService cachedDaemon(String name) { + return Executors.newCachedThreadPool(factory(name, true)); + } + + private static ThreadFactory factory(String name, boolean daemon) { + AtomicInteger counter = new AtomicInteger(0); + ThreadGroup group = new ThreadGroup(name); + return r -> { + var t = new Thread(group, r, name + "-" + counter.incrementAndGet()); + t.setDaemon(daemon); + return t; + }; + } +} diff --git a/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java b/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java index b07c0594d98..7d1494176fe 100644 --- a/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java +++ b/src/org/rascalmpl/util/base64/Base64EncodingOutputStream.java @@ -42,6 +42,7 @@ public class Base64EncodingOutputStream extends OutputStream { private final Encoder encoder; private final Base64CharWriter target; + private final Runnable onClose; private static final int ENC_INPUT_SIZE = 3 * 1024; private static final int ENC_OUTPUT_SIZE = 4 * 1024; @@ -50,9 +51,10 @@ public class Base64EncodingOutputStream extends OutputStream { private byte[] buffer = new byte[ENC_INPUT_SIZE]; private int written = 0; - public Base64EncodingOutputStream(Base64CharWriter writer, boolean padding) { + public Base64EncodingOutputStream(Base64CharWriter writer, boolean padding, Runnable onClose) { encoder = padding ? Base64.getEncoder() : Base64.getEncoder().withoutPadding(); this.target = writer; + this.onClose = onClose; } private void actualFlush() throws IOException { @@ -108,6 +110,7 @@ public void close() throws IOException { closed = true; actualFlush(); this.target.close(); + onClose.run(); } diff --git a/src/org/rascalmpl/util/base64/StreamingBase64.java b/src/org/rascalmpl/util/base64/StreamingBase64.java index d2a052059f0..fba3261316a 100644 --- a/src/org/rascalmpl/util/base64/StreamingBase64.java +++ b/src/org/rascalmpl/util/base64/StreamingBase64.java @@ -45,7 +45,18 @@ public class StreamingBase64 { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(Writer target) { - return encode(target, true); + return encode(target, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the target writer + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(Writer target, Runnable onClose) { + return encode(target, true, onClose); } /** @@ -54,7 +65,18 @@ public static OutputStream encode(Writer target) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(Writer target, boolean padding) { - return encode(Base64CharWriter.latinBytesTo(target), padding); + return encode(target, padding, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the target writer, and you can disable the optional `=` padding characters + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(Writer target, boolean padding, Runnable onClose) { + return encode(Base64CharWriter.latinBytesTo(target), padding, onClose); } /** @@ -63,7 +85,18 @@ public static OutputStream encode(Writer target, boolean padding) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(StringBuilder target) { - return encode(target, true); + return encode(target, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the StringBuilder + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(StringBuilder target, Runnable onClose) { + return encode(target, true, onClose); } /** @@ -72,11 +105,22 @@ public static OutputStream encode(StringBuilder target) { * The OutputStream needs to be closed before the target can be consumed */ public static OutputStream encode(StringBuilder target, boolean padding) { - return encode(Base64CharWriter.latinBytesTo(target), padding); + return encode(target, padding, () -> {}); + } + + /** + * Create an OutputStream that on writing bytes, encodes them to the StringBuilder, and you can disable the optional `=` padding characters + * + * The OutputStream needs to be closed before the target can be consumed + * + * The callback Runnable is run after the OutputStream closes + */ + public static OutputStream encode(StringBuilder target, boolean padding, Runnable onClose) { + return encode(Base64CharWriter.latinBytesTo(target), padding, onClose); } - private static OutputStream encode(Base64CharWriter target, boolean padding) { - return new Base64EncodingOutputStream(target, padding); + private static OutputStream encode(Base64CharWriter target, boolean padding, Runnable onClose) { + return new Base64EncodingOutputStream(target, padding, onClose); } /**