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/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/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..a7db15f66c7 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,28 @@ default boolean isChanged() { } public enum ISourceLocationChangeType { - CREATED(), - DELETED(), - MODIFIED() + CREATED(2), + DELETED(3), + MODIFIED(1); + + private final int value; + + public int getValue() { + return value; + } + + ISourceLocationChangeType(int value) { + this.value = value; + } + + public static ISourceLocationChangeType fromValue(int value) { + switch (value) { + case 2: return CREATED; + case 3: return DELETED; + case 1: return MODIFIED; + default: throw new IllegalArgumentException("Unknown ISourceLocationChangeType value " + value); + } + } } static ISourceLocationChanged created(ISourceLocation loc) { diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index ad8facf6475..8b2f6bbadd6 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,38 @@ 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) { + synchronized (this) { + if (this.externalRegistry == 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) { + synchronized (this) { + this.externalRegistry = registry; + watchers.setExternalRegistry(registry); + } } public Set getRegisteredInputSchemes() { @@ -153,46 +173,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 +382,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 +444,7 @@ private ISourceLocationInput getInputResolver(String scheme) { return result; } } - return fallbackInputResolver; + return externalRegistry; } return result; } @@ -480,7 +460,6 @@ private IClassloaderLocationResolver getClassloaderResolver(String scheme) { return result; } } - return fallbackClassloaderResolver; } return result; } @@ -496,7 +475,7 @@ private ISourceLocationOutput getOutputResolver(String scheme) { return result; } } - return fallbackOutputResolver; + return externalRegistry; } return result; } @@ -1118,7 +1097,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..b259210db7a --- /dev/null +++ b/src/org/rascalmpl/uri/remote/RascalFileSystemServices.java @@ -0,0 +1,193 @@ +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.NotDirectoryException; +import java.util.Arrays; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.codec.binary.Base64InputStream; +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.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.RemoveRequest; +import org.rascalmpl.uri.remote.jsonrpc.RenameRequest; +import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; +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 io.usethesource.vallang.ISourceLocation; + +/** + * RascalFileSystemServices offers remote access to the Rascal file system. + */ +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; + } + + @Override + public CompletableFuture resolveLocation(ISourceLocationRequest req) { + return CompletableFuture.supplyAsync(() -> { + ISourceLocation loc = req.getLocation(); + try { + ISourceLocation resolved = reg.logicalToPhysical(loc); + + if (resolved == null) { + return new SourceLocationResponse(loc); + } + + return new SourceLocationResponse(resolved); + } catch (Exception e) { + return new SourceLocationResponse(loc); + } + }, executor); + } + + @Override + public CompletableFuture watch(WatchRequest params) { + return CompletableFuture.runAsync(() -> { + try { + ISourceLocation loc = params.getLocation(); + + URIResolverRegistry.getInstance().watch(loc, params.isRecursive(), changed -> { + client.sourceLocationChanged(new ISourceLocationChanged( + changed.getLocation(), ISourceLocationChangeType.forValue(changed.getChangeType().getValue()), params.getWatcher() + )); + }); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture stat(ISourceLocationRequest req) { + return CompletableFuture.supplyAsync(() -> { + try { + return reg.stat(req.getLocation()); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture list(ISourceLocationRequest req) { + return CompletableFuture.supplyAsync(() -> { + try { + 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); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture mkDirectory(ISourceLocationRequest req) { + return CompletableFuture.runAsync(() -> { + try { + reg.mkDirectory(req.getLocation()); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture readFile(ISourceLocationRequest req) { + return CompletableFuture.supplyAsync(() -> { + try (InputStream source = new Base64InputStream(reg.getInputStream(req.getLocation()), true)) { + return new LocationContentResponse(new String(source.readAllBytes(), StandardCharsets.US_ASCII)); + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture writeFile(WriteFileRequest req) { + return CompletableFuture.runAsync(() -> { + try { + try (OutputStream target = reg.getOutputStream(req.getLocation(), req.isAppend())) { + target.write(Base64.getDecoder().decode(req.getContent())); + } + } catch (IOException | RuntimeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture remove(RemoveRequest req) { + return CompletableFuture.runAsync(() -> { + try { + reg.remove(req.getLocation(), req.isRecursive()); + } catch (IOException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture rename(RenameRequest req) { + return CompletableFuture.runAsync(() -> { + try { + reg.rename(req.getFrom(), req.getTo(), req.isOverwrite()); + } catch (IOException e) { + throw new CompletionException(e); + } + }, executor); + } +} diff --git a/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java new file mode 100644 index 00000000000..8267f0d7597 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java @@ -0,0 +1,644 @@ +/* + * 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +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.checkerframework.checker.nullness.qual.Nullable; +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 final Map watchers = new ConcurrentHashMap<>(); + private final Map watchersById = new ConcurrentHashMap<>(); + + private final int remoteResolverRegistryPort; + + public RemoteExternalResolverRegistry(int remoteResolverRegistryPort) { + this.remoteResolverRegistryPort = remoteResolverRegistryPort; + connect(); + } + + 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 ThrowingConsumer { + void accept(T t) throws E; + } + + @FunctionalInterface + private interface ThrowingTriConsumer { + void accept(T t, U u, V v) throws E; + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() 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 arg) throws IOException { + try { + return function.apply(arg); + } 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 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(ThrowingConsumer consumer, T arg) throws IOException { + try { + consumer.accept(arg); + } 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; + } + } + + private void socketExceptionCatcher(ThrowingRunnable runnable) throws IOException { + try { + runnable.run(); + } 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(NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry")) + .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 { + synchronized (cachedDirectoryListing) { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return result; + } + } + } + return call(remote::isDirectory, new ISourceLocationRequest(loc)).getValue(); + } catch (IOException e) { + return false; + } + } + + @Override + public boolean isFile(ISourceLocation loc) { + try { + synchronized (cachedDirectoryListing) { + var cached = cachedDirectoryListing.getIfPresent(URIUtil.getParentLocation(loc)); + if (cached != null) { + var result = cached.get().get(URIUtil.getLocationName(loc)); + if (result != null) { + return !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 { + synchronized (cachedDirectoryListing) { + 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 { + return new ByteArrayOutputStream() { + private boolean closed = false; + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + var content = new StringBuilder(); + try (var input = new ByteArrayInputStream(this.toByteArray())) { + StreamingBase64.encode(input, content, true); + } + synchronized (cachedDirectoryListing) { + call(remote::writeFile, new WriteFileRequest(loc, content.toString(), append)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } + } + }; + } + + @Override + public void mkDirectory(ISourceLocation loc) throws IOException { + synchronized (cachedDirectoryListing) { + call(remote::mkDirectory, new ISourceLocationRequest(loc)); + cachedDirectoryListing.invalidate(URIUtil.getParentLocation(loc)); + } + } + + @Override + public void remove(ISourceLocation loc) throws IOException { + synchronized (cachedDirectoryListing) { + 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 { + try { + synchronized (watchers) { + var key = new WatchSubscriptionKey(root, recursive); + if (!watchers.containsKey(key)) { + var freshWatchers = new Watchers(); + freshWatchers.addNewWatcher(watcher); + watchersById.put(freshWatchers.getId(), freshWatchers); + remote.watch(new WatchRequest(root, recursive, freshWatchers.getId())).get(1, TimeUnit.MINUTES); + watchers.put(key, freshWatchers); + } + watchers.get(key).addNewWatcher(watcher); + } + } catch (CompletionException | InterruptedException | ExecutionException | TimeoutException ce) { + throw new IOException("Could not watch `" + root + "` remotely: " + ce.getCause().getMessage()); + } + } + + @Override + public void unwatch(ISourceLocation root, Consumer watcher, boolean recursive) throws IOException { + var watchKey = new WatchSubscriptionKey(root, recursive); + synchronized (watchers) { + 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 (ISourceLocationChangeType.fromValue(changed.getChangeType().getValue())) { + 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)); + } + } + + private static final ExecutorService exec = NamedThreadPool.cachedDaemon("RemoteExternalResolverRegistry-watcher"); + + /** + * 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. + */ + public 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(@Nullable 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..9861074f086 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/BooleanResponse.java @@ -0,0 +1,39 @@ +/* + * 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; + +public class BooleanResponse { + private final boolean value; + + public BooleanResponse(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } +} 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..070dde2db87 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChangeType.java @@ -0,0 +1,69 @@ +/* + * 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 RuntimeException("Forgotten type: " + lsp); + } + } +} 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..b711ed6067b --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationChanged.java @@ -0,0 +1,87 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; +import org.rascalmpl.uri.ISourceLocationWatcher; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationChanged { + @NonNull + private ISourceLocation root; + @NonNull + private ISourceLocationChangeType type; + @NonNull + private String watchId; + + public ISourceLocationChanged(@NonNull ISourceLocation root, @NonNull ISourceLocationChangeType type, @NonNull 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(@Nullable 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..91ade0b861e --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/ISourceLocationRequest.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 org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class ISourceLocationRequest { + @NonNull + private ISourceLocation loc; + + + public ISourceLocationRequest(ISourceLocation loc) { + this.loc = loc; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof ISourceLocationRequest) { + return loc.equals(((ISourceLocationRequest)obj).loc); + } + return false; + } + + @Override + public int hashCode() { + return 7 * loc.hashCode(); + } + + public ISourceLocation getLocation() { + return loc; + } +} 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..57d32837d0c --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/LocationContentResponse.java @@ -0,0 +1,42 @@ +/* + * 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.checkerframework.checker.nullness.qual.NonNull; + +public class LocationContentResponse { + @NonNull + private final String content; + + public LocationContentResponse(@NonNull String content) { + this.content = content; + } + + public String getContent() { + return 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..da695f6515d --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/NumberResponse.java @@ -0,0 +1,39 @@ +/* + * 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; + +public class NumberResponse { + private final long number; + + public NumberResponse(long number) { + this.number = number; + } + + public long getNumber() { + return 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..f0dcc9ca235 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RemoveRequest.java @@ -0,0 +1,61 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; + +import io.usethesource.vallang.ISourceLocation; + +public class RemoveRequest extends ISourceLocationRequest { + private boolean recursive; + + public RemoveRequest(ISourceLocation loc, boolean recursive) { + super(loc); + this.recursive = recursive; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean equals(@Nullable 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..9355516418a --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/RenameRequest.java @@ -0,0 +1,78 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class RenameRequest { + @NonNull + private ISourceLocation from; + @NonNull + private ISourceLocation to; + + private 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(@Nullable 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..e6c6399fcf7 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SetLastModifiedRequest.java @@ -0,0 +1,60 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; + +import io.usethesource.vallang.ISourceLocation; + +public class SetLastModifiedRequest extends ISourceLocationRequest{ + private long timestamp; + + public SetLastModifiedRequest(ISourceLocation loc, long timestamp) { + super(loc); + this.timestamp = timestamp; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(@Nullable 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..d97c88db499 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/SourceLocationResponse.java @@ -0,0 +1,44 @@ +/* + * 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.checkerframework.checker.nullness.qual.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class SourceLocationResponse { + @NonNull + private final ISourceLocation loc; + + public SourceLocationResponse(@NonNull ISourceLocation loc) { + this.loc = loc; + } + + public ISourceLocation getLocation() { + return 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..894b2c269d4 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/TimestampResponse.java @@ -0,0 +1,39 @@ +/* + * 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; + +public class TimestampResponse { + private final long timestamp; + + public TimestampResponse(long timestamp) { + this.timestamp = timestamp; + } + + public long getTimestamp() { + return 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..77358f59d73 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WatchRequest.java @@ -0,0 +1,71 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class WatchRequest extends ISourceLocationRequest { + @NonNull + private String watcher; + + private boolean recursive; + + public WatchRequest(ISourceLocation loc, boolean recursive, String watcher) { + super(loc); + this.recursive = recursive; + this.watcher = watcher; + } + + public String getWatcher() { + return watcher; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof WatchRequest) { + var other = (WatchRequest)obj; + return super.equals(other) + && other.recursive == recursive + && Objects.equals(watcher, other.watcher); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), watcher, 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..5c21bd2d277 --- /dev/null +++ b/src/org/rascalmpl/uri/remote/jsonrpc/WriteFileRequest.java @@ -0,0 +1,71 @@ +/* + * 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.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +import io.usethesource.vallang.ISourceLocation; + +public class WriteFileRequest extends ISourceLocationRequest { + @NonNull + 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(@Nullable 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..a3b53fa4601 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryClient.java @@ -0,0 +1,14 @@ +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..cffc79c23c0 --- /dev/null +++ b/src/org/rascalmpl/uri/vfs/IRemoteResolverRegistryServer.java @@ -0,0 +1,184 @@ +/* + * 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.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") + default CompletableFuture readFile(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/exists") + default CompletableFuture exists(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/lastModified") + default CompletableFuture lastModified(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/created") + default CompletableFuture created(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/isDirectory") + default CompletableFuture isDirectory(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/isFile") + default CompletableFuture isFile(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/list") + default CompletableFuture list(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/size") + default CompletableFuture size(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/stat") + default CompletableFuture stat(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("input/isReadable") + default CompletableFuture isReadable(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/setLastModified") + default CompletableFuture setLastModified(SetLastModifiedRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/isWritable") + default CompletableFuture isWritable(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/writeFile") + default CompletableFuture writeFile(WriteFileRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/mkDirectory") + default CompletableFuture mkDirectory(ISourceLocationRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/remove") + default CompletableFuture remove(RemoveRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("output/rename") + default CompletableFuture rename(RenameRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("watcher/watch") + default CompletableFuture watch(WatchRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("watcher/unwatch") + default CompletableFuture unwatch(WatchRequest req) { + throw new UnsupportedOperationException(); + } + + @JsonRequest("watcher/supportsRecursiveWatch") + default CompletableFuture supportsRecursiveWatch() { + throw new UnsupportedOperationException(); + } + + @JsonRequest("logical/resolveLocation") + default CompletableFuture resolveLocation(ISourceLocationRequest loc) { + throw new UnsupportedOperationException(); + } + + 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..8253d6e8328 --- /dev/null +++ b/src/org/rascalmpl/util/Lazy.java @@ -0,0 +1,50 @@ +/* + * 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..efd38a3bd6e --- /dev/null +++ b/src/org/rascalmpl/util/NamedThreadPool.java @@ -0,0 +1,63 @@ +/* + * 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; + }; + } + +}