diff --git a/cloud/docker-image/pom.xml b/cloud/docker-image/pom.xml
index 0b37392348..a3ff265ca9 100644
--- a/cloud/docker-image/pom.xml
+++ b/cloud/docker-image/pom.xml
@@ -253,6 +253,12 @@
${project.version}
runtime
+
+ ${project.groupId}
+ filesystem-http
+ ${project.version}
+ runtime
+
${project.groupId}
metrics-stream
diff --git a/cloud/docker-image/src/main/docker/assembly.xml b/cloud/docker-image/src/main/docker/assembly.xml
index ca3954717b..7e0d989f1a 100644
--- a/cloud/docker-image/src/main/docker/assembly.xml
+++ b/cloud/docker-image/src/main/docker/assembly.xml
@@ -30,6 +30,7 @@
io/aklivity/zilla/binding-*/**
io/aklivity/zilla/catalog-*/**
io/aklivity/zilla/exporter-*/**
+ io/aklivity/zilla/filesystem-*/**
io/aklivity/zilla/guard-*/**
io/aklivity/zilla/metrics-*/**
io/aklivity/zilla/model-*/**
diff --git a/cloud/docker-image/src/main/docker/zpm.json.template b/cloud/docker-image/src/main/docker/zpm.json.template
index 46961f11bb..7a792ce44d 100644
--- a/cloud/docker-image/src/main/docker/zpm.json.template
+++ b/cloud/docker-image/src/main/docker/zpm.json.template
@@ -50,6 +50,7 @@
"io.aklivity.zilla:exporter-otlp",
"io.aklivity.zilla:exporter-prometheus",
"io.aklivity.zilla:exporter-stdout",
+ "io.aklivity.zilla:filesystem-http",
"io.aklivity.zilla:guard-jwt",
"io.aklivity.zilla:metrics-stream",
"io.aklivity.zilla:metrics-http",
diff --git a/runtime/binding-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/asyncapi/internal/config/AsyncapiBindingConfig.java b/runtime/binding-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/asyncapi/internal/config/AsyncapiBindingConfig.java
index 71054f5fb5..dd236f8f38 100644
--- a/runtime/binding-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/asyncapi/internal/config/AsyncapiBindingConfig.java
+++ b/runtime/binding-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/asyncapi/internal/config/AsyncapiBindingConfig.java
@@ -320,7 +320,6 @@ private void attachProxyBinding(
namespaceGenerator.init(binding);
final List labels = configs.stream().map(c -> c.apiLabel).collect(toList());
final NamespaceConfig composite = namespaceGenerator.generateProxy(binding, asyncapis, schemaIdsByApiId::get, labels);
- composite.readURL = binding.readURL;
attach.accept(composite);
updateNamespace(configs, composite, new ArrayList<>(asyncapis.values()));
}
@@ -349,7 +348,6 @@ private void attachServerClientBinding(
namespaceConfig.servers.forEach(s -> s.setAsyncapiProtocol(
namespaceGenerator.resolveProtocol(s.protocol(), options, namespaceConfig.asyncapis, namespaceConfig.servers)));
final NamespaceConfig composite = namespaceGenerator.generate(binding, namespaceConfig);
- composite.readURL = binding.readURL;
attach.accept(composite);
updateNamespace(namespaceConfig.configs, composite, namespaceConfig.asyncapis);
}
diff --git a/runtime/binding-echo/src/test/java/io/aklivity/zilla/runtime/binding/echo/internal/bench/EchoWorker.java b/runtime/binding-echo/src/test/java/io/aklivity/zilla/runtime/binding/echo/internal/bench/EchoWorker.java
index a2e35d4ca8..ef76fa18d2 100644
--- a/runtime/binding-echo/src/test/java/io/aklivity/zilla/runtime/binding/echo/internal/bench/EchoWorker.java
+++ b/runtime/binding-echo/src/test/java/io/aklivity/zilla/runtime/binding/echo/internal/bench/EchoWorker.java
@@ -16,8 +16,8 @@
package io.aklivity.zilla.runtime.binding.echo.internal.bench;
import java.net.InetAddress;
-import java.net.URL;
import java.nio.channels.SelectableChannel;
+import java.nio.file.Path;
import java.time.Clock;
import java.util.function.LongSupplier;
@@ -319,8 +319,8 @@ public ConverterHandler supplyWriteConverter(
}
@Override
- public URL resolvePath(
- String path)
+ public Path resolvePath(
+ String location)
{
return null;
}
diff --git a/runtime/binding-grpc/src/main/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapter.java b/runtime/binding-grpc/src/main/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapter.java
index 41d2d9131b..19e7e18acf 100644
--- a/runtime/binding-grpc/src/main/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapter.java
+++ b/runtime/binding-grpc/src/main/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapter.java
@@ -41,7 +41,7 @@ public final class GrpcOptionsConfigAdapter implements OptionsConfigAdapterSpi,
private final GrpcProtobufParser parser = new GrpcProtobufParser();
- private Function readURL;
+ private Function readResource;
@Override
public Kind kind()
@@ -88,7 +88,7 @@ public OptionsConfig adaptFromJson(
public void adaptContext(
ConfigAdapterContext context)
{
- this.readURL = context::readURL;
+ this.readResource = context::readResource;
}
private List asListProtobufs(
@@ -103,7 +103,7 @@ private GrpcProtobufConfig asProtobuf(
JsonValue value)
{
final String location = ((JsonString) value).getString();
- final String protobuf = readURL.apply(location);
+ final String protobuf = readResource.apply(location);
return parser.parse(location, protobuf);
}
diff --git a/runtime/binding-grpc/src/test/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapterTest.java b/runtime/binding-grpc/src/test/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapterTest.java
index aeb7efe820..e33893f8ff 100644
--- a/runtime/binding-grpc/src/test/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapterTest.java
+++ b/runtime/binding-grpc/src/test/java/io/aklivity/zilla/runtime/binding/grpc/internal/config/GrpcOptionsConfigAdapterTest.java
@@ -66,7 +66,7 @@ public void initJson() throws IOException
{
content = new String(resource.readAllBytes(), UTF_8);
}
- Mockito.doReturn(content).when(context).readURL("protobuf/echo.proto");
+ Mockito.doReturn(content).when(context).readResource("protobuf/echo.proto");
adapter = new OptionsConfigAdapter(OptionsConfigAdapterSpi.Kind.BINDING, context);
adapter.adaptType("grpc");
JsonbConfig config = new JsonbConfig()
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpOptionsConfig.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpOptionsConfig.java
index 40f0152347..f7e318deea 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpOptionsConfig.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpOptionsConfig.java
@@ -27,6 +27,7 @@
import io.aklivity.zilla.runtime.binding.http.internal.types.String16FW;
import io.aklivity.zilla.runtime.binding.http.internal.types.String8FW;
+import io.aklivity.zilla.runtime.engine.config.ModelConfig;
import io.aklivity.zilla.runtime.engine.config.OptionsConfig;
public final class HttpOptionsConfig extends OptionsConfig
@@ -55,41 +56,46 @@ public static HttpOptionsConfigBuilder builder(
HttpAuthorizationConfig authorization,
List requests)
{
- super(requests != null && !requests.isEmpty()
- ? requests.stream()
- .flatMap(request -> Stream.concat(
- Stream.of(request.content),
- Stream.concat(
- request.headers != null
- ? request.headers.stream().flatMap(header -> Stream.of(header != null ? header.model : null))
- : Stream.empty(),
- Stream.concat(
- request.pathParams != null
- ? request.pathParams.stream().flatMap(param -> Stream.of(param != null ? param.model : null))
- : Stream.empty(),
- Stream.concat(
- request.queryParams != null
- ? request.queryParams.stream().flatMap(param -> Stream.of(param != null ? param.model : null))
- : Stream.empty(),
- Stream.concat(request.responses != null
- ? request.responses.stream().flatMap(param -> Stream.of(param != null
- ? param.content
- : null))
- : Stream.empty(), request.responses != null
- ? request.responses.stream()
- .flatMap(response -> response.headers != null
- ? response.headers.stream()
- .flatMap(param -> Stream.of(param != null ? param.model : null))
- : Stream.empty())
- : Stream.empty())
- )))).filter(Objects::nonNull))
- .collect(Collectors.toList())
- : emptyList());
-
+ super(resolveModels(requests), List.of());
this.versions = versions;
this.overrides = overrides;
this.access = access;
this.authorization = authorization;
this.requests = requests;
}
+
+ private static List resolveModels(
+ List requests)
+ {
+ return requests != null && !requests.isEmpty()
+ ? requests.stream()
+ .flatMap(request -> Stream.concat(
+ Stream.of(request.content),
+ Stream.concat(
+ request.headers != null
+ ? request.headers.stream().flatMap(header -> Stream.of(header != null ? header.model : null))
+ : Stream.empty(),
+ Stream.concat(
+ request.pathParams != null
+ ? request.pathParams.stream().flatMap(param -> Stream.of(param != null ? param.model : null))
+ : Stream.empty(),
+ Stream.concat(
+ request.queryParams != null
+ ? request.queryParams.stream().flatMap(param -> Stream.of(param != null ? param.model : null))
+ : Stream.empty(),
+ Stream.concat(request.responses != null
+ ? request.responses.stream().flatMap(param -> Stream.of(param != null
+ ? param.content
+ : null))
+ : Stream.empty(), request.responses != null
+ ? request.responses.stream()
+ .flatMap(response -> response.headers != null
+ ? response.headers.stream()
+ .flatMap(param -> Stream.of(param != null ? param.model : null))
+ : Stream.empty())
+ : Stream.empty())
+ )))).filter(Objects::nonNull))
+ .collect(Collectors.toList())
+ : emptyList();
+ }
}
diff --git a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/config/KafkaOptionsConfig.java b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/config/KafkaOptionsConfig.java
index e597ce09ac..0d7da18c2d 100644
--- a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/config/KafkaOptionsConfig.java
+++ b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/config/KafkaOptionsConfig.java
@@ -23,6 +23,7 @@
import java.util.function.Function;
import java.util.stream.Stream;
+import io.aklivity.zilla.runtime.engine.config.ModelConfig;
import io.aklivity.zilla.runtime.engine.config.OptionsConfig;
public final class KafkaOptionsConfig extends OptionsConfig
@@ -49,15 +50,21 @@ public static KafkaOptionsConfigBuilder builder(
List servers,
KafkaSaslConfig sasl)
{
- super(topics != null && !topics.isEmpty()
- ? topics.stream()
- .flatMap(t -> Stream.of(t.key, t.value))
- .filter(Objects::nonNull)
- .collect(toList())
- : emptyList());
+ super(resolveModels(topics), List.of());
this.bootstrap = bootstrap;
this.topics = topics;
this.servers = servers;
this.sasl = sasl;
}
+
+ private static List resolveModels(
+ List topics)
+ {
+ return topics != null && !topics.isEmpty()
+ ? topics.stream()
+ .flatMap(t -> Stream.of(t.key, t.value))
+ .filter(Objects::nonNull)
+ .collect(toList())
+ : emptyList();
+ }
}
diff --git a/runtime/binding-mqtt/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/config/MqttOptionsConfig.java b/runtime/binding-mqtt/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/config/MqttOptionsConfig.java
index 1b1e9479f8..c73de19f61 100644
--- a/runtime/binding-mqtt/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/config/MqttOptionsConfig.java
+++ b/runtime/binding-mqtt/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/config/MqttOptionsConfig.java
@@ -26,6 +26,7 @@
import java.util.stream.Stream;
import io.aklivity.zilla.runtime.binding.mqtt.internal.config.MqttVersion;
+import io.aklivity.zilla.runtime.engine.config.ModelConfig;
import io.aklivity.zilla.runtime.engine.config.OptionsConfig;
public class MqttOptionsConfig extends OptionsConfig
@@ -50,18 +51,24 @@ public MqttOptionsConfig(
List topics,
List versions)
{
- super(topics != null && !topics.isEmpty()
+ super(resolveModels(topics), List.of());
+ this.authorization = authorization;
+ this.topics = topics;
+ this.versions = versions;
+ }
+
+ private static List resolveModels(
+ List topics)
+ {
+ return topics != null && !topics.isEmpty()
? topics.stream()
.flatMap(topic -> Stream.concat(
- Stream.of(topic.content),
+ Stream.of(topic.content),
Optional.ofNullable(topic.userProperties).orElseGet(Collections::emptyList).stream()
- .flatMap(p -> Stream.of(p.value))
- .filter(Objects::nonNull))
+ .flatMap(p -> Stream.of(p.value))
+ .filter(Objects::nonNull))
.filter(Objects::nonNull))
.collect(Collectors.toList())
- : emptyList());
- this.authorization = authorization;
- this.topics = topics;
- this.versions = versions;
+ : emptyList();
}
}
diff --git a/runtime/binding-openapi-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/asyncapi/internal/config/OpenapiAsyncapiBindingConfig.java b/runtime/binding-openapi-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/asyncapi/internal/config/OpenapiAsyncapiBindingConfig.java
index d7ae8c04bd..c6cb86540b 100644
--- a/runtime/binding-openapi-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/asyncapi/internal/config/OpenapiAsyncapiBindingConfig.java
+++ b/runtime/binding-openapi-asyncapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/asyncapi/internal/config/OpenapiAsyncapiBindingConfig.java
@@ -144,7 +144,6 @@ public void attach(
Object2ObjectHashMap::new));
this.composite = namespaceGenerator.generate(binding, openapis, asyncapis, openapiSchemaIdsByApiId::get);
- this.composite.readURL = binding.readURL;
attach.accept(this.composite);
BindingConfig mappingBinding = composite.bindings.stream()
diff --git a/runtime/binding-openapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/internal/config/OpenapiBindingConfig.java b/runtime/binding-openapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/internal/config/OpenapiBindingConfig.java
index 27f91d386f..a3a3d8e4aa 100644
--- a/runtime/binding-openapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/internal/config/OpenapiBindingConfig.java
+++ b/runtime/binding-openapi/src/main/java/io/aklivity/zilla/runtime/binding/openapi/internal/config/OpenapiBindingConfig.java
@@ -138,7 +138,6 @@ public void attach(
for (OpenapiNamespaceConfig namespaceConfig : namespaceConfigs.values())
{
final NamespaceConfig composite = namespaceGenerator.generate(binding, namespaceConfig);
- composite.readURL = binding.readURL;
attach.accept(composite);
namespaceConfig.configs.forEach(c ->
{
diff --git a/runtime/binding-sse/src/main/java/io/aklivity/zilla/runtime/binding/sse/config/SseOptionsConfig.java b/runtime/binding-sse/src/main/java/io/aklivity/zilla/runtime/binding/sse/config/SseOptionsConfig.java
index 427db49e49..be1bad73fb 100644
--- a/runtime/binding-sse/src/main/java/io/aklivity/zilla/runtime/binding/sse/config/SseOptionsConfig.java
+++ b/runtime/binding-sse/src/main/java/io/aklivity/zilla/runtime/binding/sse/config/SseOptionsConfig.java
@@ -23,6 +23,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import io.aklivity.zilla.runtime.engine.config.ModelConfig;
import io.aklivity.zilla.runtime.engine.config.OptionsConfig;
public final class SseOptionsConfig extends OptionsConfig
@@ -46,14 +47,20 @@ public static SseOptionsConfigBuilder builder(
int retry,
List requests)
{
- super(requests != null && !requests.isEmpty()
+ super(resolveModels(requests), List.of());
+ this.retry = retry;
+ this.requests = requests;
+ }
+
+ private static List resolveModels(
+ List requests)
+ {
+ return requests != null && !requests.isEmpty()
? requests.stream()
.flatMap(path ->
Stream.of(path.content)
.filter(Objects::nonNull))
.collect(Collectors.toList())
- : emptyList());
- this.retry = retry;
- this.requests = requests;
+ : emptyList();
}
}
diff --git a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java
index 308cdd011d..43cafdddf0 100644
--- a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java
+++ b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java
@@ -18,12 +18,10 @@
import static io.aklivity.zilla.runtime.engine.internal.stream.StreamId.isInitial;
import static java.lang.ThreadLocal.withInitial;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.agrona.LangUtil.rethrowUnchecked;
import java.net.InetAddress;
-import java.net.MalformedURLException;
-import java.net.URL;
import java.nio.channels.SelectableChannel;
+import java.nio.file.Path;
import java.time.Clock;
import java.util.function.IntConsumer;
import java.util.function.LongSupplier;
@@ -85,7 +83,7 @@ public class TlsWorker implements EngineContext
private final BindingFactory factory;
private final VaultFactory vaultFactory;
private final Configuration config;
- private final URL configURL;
+ private final Path configPath;
private final TlsSignaler signaler;
@@ -105,7 +103,7 @@ public TlsWorker(
.readonly(false)
.build()
.bufferPool();
- this.configURL = config.configURL();
+ this.configPath = Path.of(config.configURI());
this.signaler = new TlsSignaler();
@@ -387,19 +385,10 @@ public ConverterHandler supplyWriteConverter(
}
@Override
- public URL resolvePath(
- String path)
+ public Path resolvePath(
+ String location)
{
- URL resolved = null;
- try
- {
- resolved = new URL(configURL, path);
- }
- catch (MalformedURLException ex)
- {
- rethrowUnchecked(ex);
- }
- return resolved;
+ return configPath.resolveSibling(location);
}
@Override
diff --git a/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogHandler.java b/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogHandler.java
index 4716cff249..681e340594 100644
--- a/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogHandler.java
+++ b/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogHandler.java
@@ -15,7 +15,8 @@
package io.aklivity.zilla.runtime.catalog.filesystem.internal;
import java.io.InputStream;
-import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -34,7 +35,7 @@ public class FilesystemCatalogHandler implements CatalogHandler
private final CRC32C crc32c;
private final FilesystemEventContext event;
private final long catalogId;
- private final Function resolvePath;
+ private final Function resolvePath;
public FilesystemCatalogHandler(
FilesystemOptionsConfig config,
@@ -72,8 +73,8 @@ private void registerSchema(
{
try
{
- URL storeURL = resolvePath.apply(config.path);
- try (InputStream input = storeURL.openStream())
+ Path storePath = resolvePath.apply(config.path);
+ try (InputStream input = Files.newInputStream(storePath))
{
String schema = new String(input.readAllBytes());
int schemaId = generateCRC32C(schema);
diff --git a/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/config/FilesystemOptionsConfig.java b/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/config/FilesystemOptionsConfig.java
index f396581829..182995f34b 100644
--- a/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/config/FilesystemOptionsConfig.java
+++ b/runtime/catalog-filesystem/src/main/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/config/FilesystemOptionsConfig.java
@@ -14,6 +14,7 @@
*/
package io.aklivity.zilla.runtime.catalog.filesystem.internal.config;
+import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
@@ -37,6 +38,18 @@ public static FilesystemOptionsConfigBuilder builder(
public FilesystemOptionsConfig(
List subjects)
{
+ super(List.of(), resolveResources(subjects));
this.subjects = subjects;
}
+
+ private static List resolveResources(
+ List subjects)
+ {
+ List resources = new LinkedList<>();
+ for (FilesystemSchemaConfig subject : subjects)
+ {
+ resources.add(subject.path);
+ }
+ return resources;
+ }
}
diff --git a/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogFactoryTest.java b/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogFactoryTest.java
index 042367e125..cf3f010b66 100644
--- a/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogFactoryTest.java
+++ b/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemCatalogFactoryTest.java
@@ -21,6 +21,7 @@
import static org.mockito.Mockito.mock;
import java.net.URL;
+import java.nio.file.Path;
import org.junit.Test;
import org.mockito.Mockito;
@@ -38,7 +39,7 @@
public class FilesystemCatalogFactoryTest
{
@Test
- public void shouldLoadAndCreate()
+ public void shouldLoadAndCreate() throws Exception
{
Configuration config = new Configuration();
CatalogFactory factory = CatalogFactory.instantiate();
@@ -50,7 +51,8 @@ public void shouldLoadAndCreate()
EngineContext engineContext = mock(EngineContext.class);
URL url = FilesystemCatalogFactoryTest.class
.getResource("../../../../specs/catalog/filesystem/config/asyncapi/mqtt.yaml");
- Mockito.doReturn(url).when(engineContext).resolvePath("asyncapi/mqtt.yaml");
+ Path path = Path.of(url.toURI());
+ Mockito.doReturn(path).when(engineContext).resolvePath("asyncapi/mqtt.yaml");
CatalogContext context = catalog.supply(engineContext);
assertThat(context, instanceOf(FilesystemCatalogContext.class));
diff --git a/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemIT.java b/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemIT.java
index 58b6675c2d..9dff743ef2 100644
--- a/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemIT.java
+++ b/runtime/catalog-filesystem/src/test/java/io/aklivity/zilla/runtime/catalog/filesystem/internal/FilesystemIT.java
@@ -22,6 +22,7 @@
import static org.mockito.Mockito.mock;
import java.net.URL;
+import java.nio.file.Path;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
@@ -41,13 +42,14 @@ public class FilesystemIT
private EngineContext context = mock(EngineContext.class);
@Before
- public void setup()
+ public void setup() throws Exception
{
config = new FilesystemOptionsConfig(singletonList(
new FilesystemSchemaConfig("subject1", "asyncapi/mqtt.yaml")));
URL url = FilesystemIT.class.getResource("../../../../specs/catalog/filesystem/config/asyncapi/mqtt.yaml");
- Mockito.doReturn(url).when(context).resolvePath("asyncapi/mqtt.yaml");
+ Path path = Path.of(url.toURI());
+ Mockito.doReturn(path).when(context).resolvePath("asyncapi/mqtt.yaml");
}
@Test
diff --git a/runtime/catalog-karapace/pom.xml b/runtime/catalog-karapace/pom.xml
index 2b954a62c7..a39e5877af 100644
--- a/runtime/catalog-karapace/pom.xml
+++ b/runtime/catalog-karapace/pom.xml
@@ -22,7 +22,7 @@
- 0.93
+ 0.92
0
diff --git a/runtime/command-start/src/main/java/io/aklivity/zilla/runtime/command/start/internal/airline/ZillaStartCommand.java b/runtime/command-start/src/main/java/io/aklivity/zilla/runtime/command/start/internal/airline/ZillaStartCommand.java
index cc983dbb43..e72804d72a 100644
--- a/runtime/command-start/src/main/java/io/aklivity/zilla/runtime/command/start/internal/airline/ZillaStartCommand.java
+++ b/runtime/command-start/src/main/java/io/aklivity/zilla/runtime/command/start/internal/airline/ZillaStartCommand.java
@@ -25,7 +25,6 @@
import java.io.IOException;
import java.net.URI;
-import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -127,11 +126,9 @@ public void run()
EngineConfiguration config = new EngineConfiguration(props);
- URL configURL = config.configURL();
- if ("file".equals(configURL.getProtocol()))
+ Path configPath = Path.of(config.configURI());
+ if ("file".equals(configPath.getFileSystem().provider().getScheme()))
{
- final Path configPath = Paths.get(configURL.getPath());
-
if (configPath.endsWith("zilla.yaml") && Files.notExists(configPath))
{
Path configJson = configPath.resolveSibling("zilla.json");
diff --git a/runtime/engine/pom.xml b/runtime/engine/pom.xml
index 90390bf56e..ee446c0716 100644
--- a/runtime/engine/pom.xml
+++ b/runtime/engine/pom.xml
@@ -24,7 +24,7 @@
- 0.76
+ 0.74
5
@@ -77,6 +77,11 @@
jackson-dataformat-yaml
2.16.1
+
+ ${project.groupId}
+ filesystem-http
+ test
+
org.jmock
jmock-junit4
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/Engine.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/Engine.java
index c504dc9531..78085f8e9a 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/Engine.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/Engine.java
@@ -16,21 +16,11 @@
package io.aklivity.zilla.runtime.engine;
import static io.aklivity.zilla.runtime.engine.internal.layouts.metrics.HistogramsLayout.BUCKETS;
-import static java.net.http.HttpClient.Redirect.NORMAL;
-import static java.net.http.HttpClient.Version.HTTP_2;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static java.util.stream.Collectors.toList;
import static org.agrona.LangUtil.rethrowUnchecked;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URISyntaxException;
import java.net.URL;
-import java.net.URLConnection;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -40,11 +30,9 @@
import java.util.ServiceLoader.Provider;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.LongConsumer;
import java.util.function.LongSupplier;
@@ -71,9 +59,6 @@
import io.aklivity.zilla.runtime.engine.internal.layouts.EventsLayout;
import io.aklivity.zilla.runtime.engine.internal.registry.EngineManager;
import io.aklivity.zilla.runtime.engine.internal.registry.EngineWorker;
-import io.aklivity.zilla.runtime.engine.internal.registry.FileWatcherTask;
-import io.aklivity.zilla.runtime.engine.internal.registry.HttpWatcherTask;
-import io.aklivity.zilla.runtime.engine.internal.registry.WatcherTask;
import io.aklivity.zilla.runtime.engine.internal.types.event.EventFW;
import io.aklivity.zilla.runtime.engine.metrics.Collector;
import io.aklivity.zilla.runtime.engine.metrics.MetricGroup;
@@ -92,15 +77,11 @@ public final class Engine implements Collector, AutoCloseable
private final AtomicInteger nextTaskId;
private final ThreadFactory factory;
- private final WatcherTask watcherTask;
- private final URL configURL;
private final List workers;
private final boolean readonly;
private final EngineConfiguration config;
private final EngineManager manager;
- private Future watcherTaskRef;
-
Engine(
EngineConfiguration config,
Collection bindings,
@@ -205,24 +186,7 @@ public final class Engine implements Collector, AutoCloseable
logger,
context,
config,
- extensions,
- this::readURL);
-
- this.configURL = config.configURL();
- String protocol = configURL.getProtocol();
- if ("file".equals(protocol) || "jar".equals(protocol))
- {
- Function watcherReadURL = l -> readURL(configURL, l);
- this.watcherTask = new FileWatcherTask(manager::reconfigure, watcherReadURL);
- }
- else if ("http".equals(protocol) || "https".equals(protocol))
- {
- this.watcherTask = new HttpWatcherTask(manager::reconfigure, config.configPollIntervalSeconds());
- }
- else
- {
- throw new UnsupportedOperationException();
- }
+ extensions);
this.bindings = bindings;
this.tasks = tasks;
@@ -255,11 +219,10 @@ public void start() throws Exception
worker.doStart();
}
- watcherTaskRef = watcherTask.submit();
+ // ignore the config file in read-only mode; no config will be read so no namespaces, bindings, etc. will be attached
if (!readonly)
{
- // ignore the config file in read-only mode; no config will be read so no namespaces, bindings, etc will be attached
- watcherTask.watch(configURL).get();
+ manager.start();
}
}
@@ -273,8 +236,7 @@ public void close() throws Exception
final List errors = new ArrayList<>();
- watcherTask.close();
- watcherTaskRef.get();
+ manager.close();
for (EngineWorker worker : workers)
{
@@ -316,49 +278,6 @@ public static EngineBuilder builder()
return new EngineBuilder();
}
- private String readURL(
- URL configURL,
- String location)
- {
- String output = null;
- try
- {
- final URL fileURL = new URL(configURL, location);
- if ("http".equals(fileURL.getProtocol()) || "https".equals(fileURL.getProtocol()))
- {
- HttpClient client = HttpClient.newBuilder()
- .version(HTTP_2)
- .followRedirects(NORMAL)
- .build();
-
- HttpRequest request = HttpRequest.newBuilder()
- .GET()
- .uri(fileURL.toURI())
- .build();
-
- HttpResponse response = client.send(
- request,
- HttpResponse.BodyHandlers.ofString());
-
- output = response.body();
- }
- else
- {
-
- URLConnection connection = fileURL.openConnection();
- try (InputStream input = connection.getInputStream())
- {
- output = new String(input.readAllBytes(), UTF_8);
- }
- }
- }
- catch (IOException | URISyntaxException | InterruptedException ex)
- {
- output = "";
- }
- return output;
- }
-
private Thread newTaskThread(
Runnable r)
{
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineConfiguration.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineConfiguration.java
index 5f9522446d..61e17d3710 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineConfiguration.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineConfiguration.java
@@ -19,6 +19,7 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import java.io.File;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
@@ -44,6 +45,7 @@ public class EngineConfiguration extends Configuration
public static final boolean DEBUG_BUDGETS = Boolean.getBoolean("zilla.engine.debug.budgets");
public static final PropertyDef ENGINE_CONFIG_URL;
+ public static final PropertyDef ENGINE_CONFIG_URI;
public static final IntPropertyDef ENGINE_CONFIG_POLL_INTERVAL_SECONDS;
public static final PropertyDef ENGINE_NAME;
public static final PropertyDef ENGINE_DIRECTORY;
@@ -81,6 +83,8 @@ public class EngineConfiguration extends Configuration
{
final ConfigurationDef config = new ConfigurationDef("zilla.engine");
ENGINE_CONFIG_URL = config.property(URL.class, "config.url", EngineConfiguration::configURL, "file:zilla.yaml");
+ ENGINE_CONFIG_URI = config.property(URI.class, "config.uri", EngineConfiguration::decodeConfigURI,
+ EngineConfiguration::defaultConfigURI);
ENGINE_CONFIG_POLL_INTERVAL_SECONDS = config.property("config.poll.interval.seconds", 60);
ENGINE_NAME = config.property("name", EngineConfiguration::defaultName);
ENGINE_DIRECTORY = config.property("directory", EngineConfiguration::defaultDirectory);
@@ -141,11 +145,17 @@ public EngineConfiguration()
super(ENGINE_CONFIG, new Configuration());
}
+ @Deprecated
public URL configURL()
{
return ENGINE_CONFIG_URL.get(this);
}
+ public URI configURI()
+ {
+ return ENGINE_CONFIG_URI.get(this);
+ }
+
public int configPollIntervalSeconds()
{
return ENGINE_CONFIG_POLL_INTERVAL_SECONDS.getAsInt(this);
@@ -314,8 +324,7 @@ private static int defaultBudgetsBufferCapacity(
private static URL configURL(
Configuration config,
- String url
- )
+ String url)
{
URL configURL = null;
try
@@ -416,4 +425,20 @@ private static HostResolver defaultHostResolver(
return addresses;
};
}
+
+ private static URI decodeConfigURI(
+ Configuration config,
+ String value)
+ {
+ return value.startsWith("file:")
+ ? new File(value.substring("file:".length())).toURI()
+ : URI.create(value);
+ }
+
+ private static URI defaultConfigURI(
+ Configuration config)
+ {
+ URL url = ENGINE_CONFIG_URL.get(config);
+ return decodeConfigURI(config, url.toString());
+ }
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineContext.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineContext.java
index 3c6f81931e..c810d29998 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineContext.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/EngineContext.java
@@ -16,8 +16,8 @@
package io.aklivity.zilla.runtime.engine;
import java.net.InetAddress;
-import java.net.URL;
import java.nio.channels.SelectableChannel;
+import java.nio.file.Path;
import java.time.Clock;
import java.util.function.LongSupplier;
@@ -157,8 +157,8 @@ ConverterHandler supplyReadConverter(
ConverterHandler supplyWriteConverter(
ModelConfig config);
- URL resolvePath(
- String path);
+ Path resolvePath(
+ String location);
Metric resolveMetric(
String name);
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/BindingConfig.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/BindingConfig.java
index 33faaec460..0eb2852e64 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/BindingConfig.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/BindingConfig.java
@@ -27,7 +27,6 @@ public class BindingConfig
public transient long id;
public transient long entryId;
public transient ToLongFunction resolveId;
- public transient Function readURL;
public transient long vaultId;
public transient String qvault;
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/ConfigAdapterContext.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/ConfigAdapterContext.java
index fa4f5161e9..bddd2c77f5 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/ConfigAdapterContext.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/ConfigAdapterContext.java
@@ -15,7 +15,9 @@
*/
package io.aklivity.zilla.runtime.engine.config;
+@Deprecated
public interface ConfigAdapterContext
{
- String readURL(String location);
+ String readResource(
+ String location);
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigBuilder.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigBuilder.java
index 5c4f01df83..914ff3ca76 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigBuilder.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigBuilder.java
@@ -68,7 +68,6 @@ public T build()
namespaces = new LinkedList<>();
}
- return mapper.apply(new EngineConfig(
- namespaces));
+ return mapper.apply(new EngineConfig(namespaces));
}
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigReader.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigReader.java
index 5b1aa71d27..b2be03ac84 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigReader.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/EngineConfigReader.java
@@ -60,7 +60,6 @@ public final class EngineConfigReader
private final Collection schemaTypes;
private final Consumer logger;
-
public EngineConfigReader(
EngineConfiguration config,
ConfigAdapterContext context,
@@ -161,7 +160,8 @@ public EngineConfig read(
{
reader.reset();
reader.skip(configAt);
- builder.namespace(jsonb.fromJson(reader, NamespaceConfig.class));
+ NamespaceConfig namespace = jsonb.fromJson(reader, NamespaceConfig.class);
+ builder.namespace(namespace);
if (!errors.isEmpty())
{
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/GuardConfig.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/GuardConfig.java
index 5804edd815..5363211bfd 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/GuardConfig.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/GuardConfig.java
@@ -18,12 +18,9 @@
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
-import java.util.function.Function;
-
public class GuardConfig
{
public transient long id;
- public transient Function readURL;
public final String namespace;
public final String name;
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/NamespaceConfig.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/NamespaceConfig.java
index a98f862482..16e6fe30f8 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/NamespaceConfig.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/NamespaceConfig.java
@@ -18,13 +18,14 @@
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
+import java.util.LinkedList;
import java.util.List;
-import java.util.function.Function;
public class NamespaceConfig
{
+ public static final String FILESYSTEM = "filesystem";
+
public transient int id;
- public transient Function readURL;
public final String name;
public final TelemetryConfig telemetry;
@@ -32,6 +33,7 @@ public class NamespaceConfig
public final List guards;
public final List vaults;
public final List catalogs;
+ public final List resources;
public static NamespaceConfigBuilder builder()
{
@@ -52,5 +54,52 @@ public static NamespaceConfigBuilder builder()
this.guards = requireNonNull(guards);
this.vaults = requireNonNull(vaults);
this.catalogs = requireNonNull(catalogs);
+ this.resources = resolveResources(this, telemetry, bindings, guards, vaults, catalogs);
+ }
+
+ private static List resolveResources(
+ NamespaceConfig namespace,
+ TelemetryConfig telemetry,
+ List bindings,
+ List guards,
+ List vaults,
+ List catalogs)
+ {
+ List options = new LinkedList<>();
+
+ if (telemetry != null && telemetry.exporters != null)
+ {
+ telemetry.exporters.stream()
+ .filter(e -> e.options != null)
+ .map(e -> e.options)
+ .forEach(options::add);
+ }
+
+ bindings.stream()
+ .filter(b -> b.options != null)
+ .map(b -> b.options)
+ .forEach(options::add);
+
+ guards.stream()
+ .filter(g -> g.options != null)
+ .map(g -> g.options)
+ .forEach(options::add);
+
+ vaults.stream()
+ .filter(v -> v.options != null)
+ .map(v -> v.options)
+ .forEach(options::add);
+
+ catalogs.stream()
+ .filter(c -> c.options != null)
+ .map(c -> c.options)
+ .forEach(options::add);
+
+ return options.stream()
+ .filter(o -> o.resources != null)
+ .flatMap(o -> o.resources.stream())
+ .sorted()
+ .distinct()
+ .toList();
}
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/OptionsConfig.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/OptionsConfig.java
index 2e83dcc8f6..7eba0cd095 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/OptionsConfig.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/config/OptionsConfig.java
@@ -21,15 +21,18 @@
public class OptionsConfig
{
public final List models;
+ public final List resources;
public OptionsConfig()
{
- this(Collections.emptyList());
+ this(Collections.emptyList(), Collections.emptyList());
}
public OptionsConfig(
- List models)
+ List models,
+ List resources)
{
this.models = models;
+ this.resources = resources;
}
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineManager.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineManager.java
index 425f9d5113..6666cd6c78 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineManager.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineManager.java
@@ -18,14 +18,17 @@
import static java.util.stream.Collectors.toList;
import static org.agrona.LangUtil.rethrowUnchecked;
+import java.io.IOException;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
-import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
@@ -62,6 +65,7 @@
import io.aklivity.zilla.runtime.engine.guard.Guard;
import io.aklivity.zilla.runtime.engine.internal.Tuning;
import io.aklivity.zilla.runtime.engine.internal.config.NamespaceAdapter;
+import io.aklivity.zilla.runtime.engine.internal.watcher.EngineConfigWatchTask;
import io.aklivity.zilla.runtime.engine.namespace.NamespacedId;
import io.aklivity.zilla.runtime.engine.resolver.Resolver;
@@ -81,9 +85,11 @@ public class EngineManager
private final EngineExtContext context;
private final EngineConfiguration config;
private final List extensions;
- private final BiFunction readURL;
private final Resolver expressions;
+ private final Path configPath;
+ private final EngineConfigWatchTask watchTask;
+ private String currentText;
private EngineConfig current;
public EngineManager(
@@ -98,8 +104,7 @@ public EngineManager(
Consumer logger,
EngineExtContext context,
EngineConfiguration config,
- List extensions,
- BiFunction readURL)
+ List extensions)
{
this.schemaTypes = schemaTypes;
this.bindingByType = bindingByType;
@@ -113,34 +118,73 @@ public EngineManager(
this.context = context;
this.config = config;
this.extensions = extensions;
- this.readURL = readURL;
this.expressions = Resolver.instantiate(config);
+ this.configPath = Path.of(config.configURI());
+ this.watchTask = new WatchTaskImpl(configPath);
}
- public EngineConfig reconfigure(
- URL configURL,
- String configText)
+ public void start() throws Exception
+ {
+ watchTask.submit();
+ }
+
+ public void close()
+ {
+ watchTask.close();
+ }
+
+ public void process(
+ NamespaceConfig namespace)
+ {
+ final List guards = current.namespaces.stream()
+ .map(n -> n.guards)
+ .flatMap(gs -> gs.stream())
+ .collect(toList());
+
+ process(guards, namespace);
+ }
+
+ private void onPathChanged(
+ Path watchedPath)
{
EngineConfig newConfig = null;
+ reconfigure:
try
{
- newConfig = parse(configURL, configText);
+ if (!Files.exists(watchedPath))
+ {
+ break reconfigure;
+ }
+
+ String newConfigText = Files.readString(configPath);
+ if (Objects.equals(currentText, newConfigText))
+ {
+ break reconfigure;
+ }
+
+ newConfig = parse(newConfigText);
if (newConfig != null)
{
+ final String oldConfigText = currentText;
final EngineConfig oldConfig = current;
+
unregister(oldConfig);
try
{
+ currentText = newConfigText;
current = newConfig;
+
register(newConfig);
}
catch (Exception ex)
{
context.onError(ex);
+ currentText = oldConfigText;
current = oldConfig;
+
register(oldConfig);
rethrowUnchecked(ex);
@@ -159,23 +203,9 @@ public EngineConfig reconfigure(
throw new ConfigException("Engine configuration failed", ex);
}
}
-
- return newConfig;
- }
-
- public void process(
- NamespaceConfig namespace)
- {
- final List guards = current.namespaces.stream()
- .map(n -> n.guards)
- .flatMap(gs -> gs.stream())
- .collect(toList());
-
- process(guards, namespace);
}
private EngineConfig parse(
- URL configURL,
String configText)
{
EngineConfig engine = null;
@@ -189,11 +219,9 @@ private EngineConfig parse(
try
{
- final Function namespaceReadURL = l -> readURL.apply(configURL, l);
-
EngineConfigReader reader = new EngineConfigReader(
config,
- new NamespaceConfigAdapterContext(namespaceReadURL),
+ new NamespaceConfigAdapterContext(Path.of(config.configURI())),
expressions,
schemaTypes,
logger);
@@ -207,7 +235,6 @@ private EngineConfig parse(
for (NamespaceConfig namespace : engine.namespaces)
{
- namespace.readURL = l -> readURL.apply(configURL, l);
process(guards, namespace);
}
}
@@ -223,8 +250,6 @@ private void process(
List guards,
NamespaceConfig namespace)
{
- assert namespace.readURL != null;
-
namespace.id = supplyId.applyAsInt(namespace.name);
NameResolver resolver = new NameResolver(namespace.id);
@@ -232,7 +257,6 @@ private void process(
for (GuardConfig guard : namespace.guards)
{
guard.id = resolver.resolve(guard.name);
- guard.readURL = namespace.readURL;
}
for (VaultConfig vault : namespace.vaults)
@@ -264,7 +288,6 @@ private void process(
binding.id = resolver.resolve(binding.name);
binding.entryId = resolver.resolve(binding.entry);
binding.resolveId = resolver::resolve;
- binding.readURL = namespace.readURL;
binding.typeId = supplyId.applyAsInt(binding.type);
binding.kindId = supplyId.applyAsInt(binding.kind.name().toLowerCase());
@@ -377,6 +400,7 @@ private void register(
for (NamespaceConfig namespace : config.namespaces)
{
register(namespace);
+ watch(namespace);
}
}
@@ -390,6 +414,7 @@ private void unregister(
{
for (NamespaceConfig namespace : config.namespaces)
{
+ unwatch(namespace);
unregister(namespace);
}
}
@@ -397,6 +422,20 @@ private void unregister(
extensions.forEach(e -> e.onUnregistered(context));
}
+ private void watch(
+ NamespaceConfig namespace)
+ {
+ namespace.resources.stream()
+ .forEach(watchTask::watch);
+ }
+
+ private void unwatch(
+ NamespaceConfig namespace)
+ {
+ namespace.resources.stream()
+ .forEach(watchTask::unwatch);
+ }
+
private void register(
NamespaceConfig namespace)
{
@@ -459,19 +498,47 @@ private String format(
private static final class NamespaceConfigAdapterContext implements ConfigAdapterContext
{
- private final Function readURL;
+ private final Path configPath;
NamespaceConfigAdapterContext(
- Function readURL)
+ Path configPath)
{
- this.readURL = readURL;
+ this.configPath = configPath;
}
@Override
- public String readURL(
+ public String readResource(
String location)
{
- return readURL.apply(location);
+ String content = null;
+
+ try
+ {
+ Path path = configPath.resolveSibling(location);
+ content = Files.readString(path);
+ }
+ catch (IOException ex)
+ {
+ rethrowUnchecked(ex);
+ }
+
+ return content;
+ }
+ }
+
+ private final class WatchTaskImpl extends EngineConfigWatchTask
+ {
+ WatchTaskImpl(
+ Path configPath)
+ {
+ super(configPath);
+ }
+
+ @Override
+ protected void onPathChanged(
+ Path watchedPath)
+ {
+ EngineManager.this.onPathChanged(watchedPath);
}
}
}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java
index bc4b731109..69262daf48 100644
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java
@@ -37,13 +37,12 @@
import static java.lang.ThreadLocal.withInitial;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.agrona.CloseHelper.quietClose;
-import static org.agrona.LangUtil.rethrowUnchecked;
import static org.agrona.concurrent.AgentRunner.startOnThread;
import java.net.InetAddress;
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
import java.nio.channels.SelectableChannel;
+import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.util.BitSet;
@@ -173,7 +172,6 @@ public class EngineWorker implements EngineContext, Agent
private final int localIndex;
private final EngineConfiguration config;
- private final URL configURL;
private final LabelManager labels;
private final String agentName;
private final Function resolveHost;
@@ -217,6 +215,7 @@ public class EngineWorker implements EngineContext, Agent
private final EngineRegistry registry;
private final Deque taskQueue;
private final LongUnaryOperator affinityMask;
+ private final Path configPath;
private final AgentRunner runner;
private final IdleStrategy idleStrategy;
private final ErrorHandler errorHandler;
@@ -260,7 +259,7 @@ public EngineWorker(
{
this.localIndex = index;
this.config = config;
- this.configURL = config.configURL();
+ this.configPath = Path.of(config.configURI());
this.labels = labels;
this.affinityMask = affinityMask;
@@ -741,19 +740,12 @@ public ConverterHandler supplyWriteConverter(
}
@Override
- public URL resolvePath(
- String path)
+ public Path resolvePath(
+ String location)
{
- URL resolved = null;
- try
- {
- resolved = new URL(configURL, path);
- }
- catch (MalformedURLException ex)
- {
- rethrowUnchecked(ex);
- }
- return resolved;
+ return location.indexOf(':') == -1
+ ? configPath.resolveSibling(location)
+ : Path.of(URI.create(location));
}
@Override
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/FileWatcherTask.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/FileWatcherTask.java
deleted file mode 100644
index fbcc466fb5..0000000000
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/FileWatcherTask.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2021-2023 Aklivity Inc.
- *
- * Aklivity licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package io.aklivity.zilla.runtime.engine.internal.registry;
-
-import static org.agrona.LangUtil.rethrowUnchecked;
-
-import java.io.IOException;
-import java.net.URL;
-import java.nio.file.ClosedWatchServiceException;
-import java.nio.file.FileSystems;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-import io.aklivity.zilla.runtime.engine.config.EngineConfig;
-
-public class FileWatcherTask extends WatcherTask
-{
- private final Map watchedConfigs;
- private final WatchService watchService;
- private final Function readURL;
-
- public FileWatcherTask(
- BiFunction changeListener,
- Function readURL)
- {
- super(changeListener);
- this.readURL = readURL;
- this.watchedConfigs = new IdentityHashMap<>();
- WatchService watchService = null;
-
- try
- {
- watchService = FileSystems.getDefault().newWatchService();
- }
- catch (IOException ex)
- {
- rethrowUnchecked(ex);
- }
-
- this.watchService = watchService;
-
- }
-
- @Override
- public Future submit()
- {
- return executor.submit(this);
- }
-
- @Override
- public Void call()
- {
- while (true)
- {
- try
- {
- final WatchKey key = watchService.take();
-
- WatchedConfig watchedConfig = watchedConfigs.get(key);
-
- if (watchedConfig != null && watchedConfig.isWatchedKey(key))
- {
- // Even if no reconfigure needed, recalculation is necessary, since symlinks might have changed.
- watchedConfig.keys().forEach(watchedConfigs::remove);
- watchedConfig.unregister();
- watchedConfig.register();
- watchedConfig.keys().forEach(k -> watchedConfigs.put(k, watchedConfig));
- String newConfigText = readURL.apply(watchedConfig.getURL().toString());
- byte[] newConfigHash = computeHash(newConfigText);
- if (watchedConfig.isReconfigureNeeded(newConfigHash))
- {
- watchedConfig.setConfigHash(newConfigHash);
- changeListener.apply(watchedConfig.getURL(), newConfigText);
- }
- }
- }
- catch (InterruptedException | ClosedWatchServiceException ex)
- {
- break;
- }
- }
-
- return null;
- }
-
- @Override
- public CompletableFuture watch(
- URL configURL)
- {
- WatchedConfig watchedConfig = new WatchedConfig(configURL, watchService);
- watchedConfig.register();
- watchedConfig.keys().forEach(k -> watchedConfigs.put(k, watchedConfig));
- String configText = readURL.apply(configURL.toString());
- watchedConfig.setConfigHash(computeHash(configText));
-
- CompletableFuture configFuture;
- try
- {
- EngineConfig config = changeListener.apply(configURL, configText);
- configFuture = CompletableFuture.completedFuture(config);
- }
- catch (Exception ex)
- {
- configFuture = CompletableFuture.failedFuture(ex);
- }
-
- return configFuture;
- }
-
- @Override
- public void close() throws IOException
- {
- watchService.close();
- }
-}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/HttpWatcherTask.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/HttpWatcherTask.java
deleted file mode 100644
index c4567ad7fa..0000000000
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/HttpWatcherTask.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright 2021-2023 Aklivity Inc.
- *
- * Aklivity licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package io.aklivity.zilla.runtime.engine.internal.registry;
-
-import static java.net.http.HttpClient.Redirect.NORMAL;
-import static java.net.http.HttpClient.Version.HTTP_2;
-import static org.agrona.LangUtil.rethrowUnchecked;
-
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.function.BiFunction;
-
-import io.aklivity.zilla.runtime.engine.config.EngineConfig;
-
-public class HttpWatcherTask extends WatcherTask
-{
- private static final URI CLOSE_REQUESTED = URI.create("http://localhost:12345");
-
- private final Map etags;
- private final Map configHashes;
- private final Map> futures;
- private final BlockingQueue configQueue;
- private final int pollSeconds;
-
- public HttpWatcherTask(
- BiFunction changeListener,
- int pollSeconds)
- {
- super(changeListener);
- this.etags = new ConcurrentHashMap<>();
- this.configHashes = new ConcurrentHashMap<>();
- this.futures = new ConcurrentHashMap<>();
- this.configQueue = new LinkedBlockingQueue<>();
- this.pollSeconds = pollSeconds;
- }
-
- @Override
- public Future submit()
- {
- return executor.submit(this);
- }
-
- @Override
- public Void call() throws InterruptedException
- {
- while (true)
- {
- URI configURI = configQueue.take();
- if (configURI == CLOSE_REQUESTED)
- {
- break;
- }
- String etag = etags.getOrDefault(configURI, "");
- sendAsync(configURI, etag);
- }
- return null;
- }
-
- @Override
- public CompletableFuture watch(
- URL configURL)
- {
- URI configURI = toURI(configURL);
-
- CompletableFuture configFuture;
- try
- {
- EngineConfig config = sendSync(configURI);
- configFuture = CompletableFuture.completedFuture(config);
- }
- catch (Exception ex)
- {
- configFuture = CompletableFuture.failedFuture(ex);
- }
-
- return configFuture;
- }
-
- @Override
- public void close()
- {
- futures.values().forEach(future -> future.cancel(true));
- configQueue.add(CLOSE_REQUESTED);
- }
-
- private EngineConfig sendSync(
- URI configURI)
- {
- HttpClient client = HttpClient.newBuilder()
- .version(HTTP_2)
- .followRedirects(NORMAL)
- .build();
- HttpRequest request = HttpRequest.newBuilder()
- .GET()
- .uri(configURI)
- .build();
- HttpResponse response;
- try
- {
- response = client.send(request, HttpResponse.BodyHandlers.ofString());
- }
- catch (Exception ex)
- {
- handleException(ex, configURI);
- return null;
- }
- return handleConfigChange(response);
- }
-
- private void sendAsync(
- URI configURI,
- String etag)
- {
- HttpClient client = HttpClient.newBuilder()
- .version(HTTP_2)
- .followRedirects(NORMAL)
- .build();
- HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
- .GET()
- .uri(configURI);
- if (etag != null && !etag.isEmpty())
- {
- requestBuilder = requestBuilder.headers("If-None-Match", etag, "Prefer", "wait=86400");
- }
-
- CompletableFuture future = client.sendAsync(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
- .thenAccept(this::handleConfigChange)
- .exceptionally(ex -> handleException(ex, configURI));
- futures.put(configURI, future);
- }
-
- private Void handleException(
- Throwable throwable,
- URI configURI)
- {
- scheduleRequest(configURI, pollSeconds);
- return null;
- }
-
- private EngineConfig handleConfigChange(
- HttpResponse response)
- {
- EngineConfig config = null;
- try
- {
- URI configURI = response.request().uri();
- int statusCode = response.statusCode();
- int pollIntervalSeconds = 0;
- if (statusCode == 404)
- {
- config = changeListener.apply(configURI.toURL(), "");
- pollIntervalSeconds = this.pollSeconds;
- }
- else if (statusCode >= 500 && statusCode <= 599)
- {
- pollIntervalSeconds = this.pollSeconds;
- }
- else
- {
- Optional etagOptional = response.headers().firstValue("Etag");
- String configText = response.body();
-
- if (etagOptional.isPresent())
- {
- String oldEtag = etags.getOrDefault(configURI, "");
- if (!oldEtag.equals(etagOptional.get()))
- {
- etags.put(configURI, etagOptional.get());
- config = changeListener.apply(configURI.toURL(), configText);
- }
- else if (response.statusCode() != 304)
- {
- pollIntervalSeconds = this.pollSeconds;
- }
- }
- else
- {
- byte[] configHash = configHashes.get(configURI);
- byte[] newConfigHash = computeHash(configText);
- if (!Arrays.equals(configHash, newConfigHash))
- {
- configHashes.put(configURI, newConfigHash);
- config = changeListener.apply(configURI.toURL(), configText);
- }
- pollIntervalSeconds = this.pollSeconds;
- }
- }
- futures.remove(configURI);
- scheduleRequest(configURI, pollIntervalSeconds);
- }
- catch (MalformedURLException ex)
- {
- rethrowUnchecked(ex);
- }
- return config;
- }
-
- private void scheduleRequest(URI configURI, int pollIntervalSeconds)
- {
- if (pollIntervalSeconds == 0)
- {
- configQueue.add(configURI);
- }
- else
- {
- executor.schedule(() -> configQueue.add(configURI), pollIntervalSeconds, TimeUnit.SECONDS);
- }
- }
-
- private URI toURI(
- URL configURL)
- {
- URI configURI = null;
- try
- {
- configURI = configURL.toURI();
- }
- catch (URISyntaxException ex)
- {
- rethrowUnchecked(ex);
- }
- return configURI;
- }
-}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatchedConfig.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatchedConfig.java
deleted file mode 100644
index a6ee866978..0000000000
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatchedConfig.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2021-2023 Aklivity Inc.
- *
- * Aklivity licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package io.aklivity.zilla.runtime.engine.internal.registry;
-
-import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
-import static org.agrona.LangUtil.rethrowUnchecked;
-
-import java.io.IOException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.util.Arrays;
-import java.util.Deque;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Set;
-
-public class WatchedConfig
-{
- private final WatchService watchService;
- private final Set watchKeys;
- private final URL configURL;
- private byte[] configHash;
-
- public WatchedConfig(
- URL configURL,
- WatchService watchService)
- {
- this.watchService = watchService;
- this.watchKeys = new HashSet<>();
- this.configURL = configURL;
- }
-
- public Set keys()
- {
- return watchKeys;
- }
-
- public void register()
- {
- Path configPath = Paths.get(configURL.getPath()).toAbsolutePath();
- try
- {
- Set watchedPaths = new HashSet<>();
-
- Deque observablePaths = new LinkedList<>();
- observablePaths.addLast(configPath);
-
- while (!observablePaths.isEmpty())
- {
- Path observablePath = observablePaths.removeFirst();
-
- if (watchedPaths.add(observablePath))
- {
- if (Files.isSymbolicLink(observablePath))
- {
- Path targetPath = Files.readSymbolicLink(observablePath);
- targetPath = configPath.resolveSibling(targetPath).normalize();
- observablePaths.addLast(targetPath);
- }
-
- for (Path ancestorPath = observablePath.getParent();
- ancestorPath != null;
- ancestorPath = ancestorPath.getParent())
- {
- if (Files.isSymbolicLink(ancestorPath))
- {
- if (watchedPaths.add(ancestorPath))
- {
- Path targetPath = Files.readSymbolicLink(ancestorPath);
- observablePaths.addLast(ancestorPath.resolve(targetPath).normalize());
- }
- }
- }
- }
- }
-
- for (Path watchedPath : watchedPaths)
- {
- if (Files.exists(watchedPath.getParent()))
- {
- WatchKey key = registerPath(watchedPath.getParent());
- watchKeys.add(key);
- }
- }
- }
- catch (IOException ex)
- {
- rethrowUnchecked(ex);
- }
- }
-
- public void unregister()
- {
- watchKeys.forEach(WatchKey::cancel);
- watchKeys.clear();
- }
-
- public boolean isWatchedKey(
- WatchKey key)
- {
- return watchKeys.contains(key);
- }
-
- public boolean isReconfigureNeeded(
- byte[] newConfigHash)
- {
- return !Arrays.equals(configHash, newConfigHash);
- }
-
- public void setConfigHash(
- byte[] newConfigHash)
- {
- configHash = newConfigHash;
- }
-
- public URL getURL()
- {
- return configURL;
- }
-
- private WatchKey registerPath(
- Path configPath)
- {
- WatchKey key = null;
- try
- {
- key = configPath.register(watchService, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
- }
- catch (IOException ex)
- {
- rethrowUnchecked(ex);
- }
- return key;
- }
-
- private Path toRealPath(
- Path configPath)
- {
- try
- {
- configPath = configPath.toRealPath();
- }
- catch (IOException ex)
- {
- rethrowUnchecked(ex);
- }
- return configPath;
- }
-
-}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatcherTask.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatcherTask.java
deleted file mode 100644
index 0bfc9e64e5..0000000000
--- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/WatcherTask.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2021-2023 Aklivity Inc.
- *
- * Aklivity licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package io.aklivity.zilla.runtime.engine.internal.registry;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.agrona.LangUtil.rethrowUnchecked;
-
-import java.io.Closeable;
-import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.function.BiFunction;
-
-import io.aklivity.zilla.runtime.engine.config.EngineConfig;
-
-public abstract class WatcherTask implements Callable, Closeable
-{
- private final MessageDigest md5;
-
- protected final ScheduledExecutorService executor;
- protected final BiFunction changeListener;
-
- protected WatcherTask(
- BiFunction changeListener)
- {
- this.changeListener = changeListener;
- this.md5 = initMessageDigest("MD5");
- this.executor = Executors.newScheduledThreadPool(2);
- }
-
- public abstract Future submit();
-
- public abstract CompletableFuture watch(
- URL configURL);
-
- protected byte[] computeHash(
- String configText)
- {
- return md5.digest(configText.getBytes(UTF_8));
- }
-
- private MessageDigest initMessageDigest(
- String algorithm)
- {
- MessageDigest md5 = null;
- try
- {
- md5 = MessageDigest.getInstance(algorithm);
- }
- catch (NoSuchAlgorithmException ex)
- {
- rethrowUnchecked(ex);
- }
- return md5;
- }
-}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatchTask.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatchTask.java
new file mode 100644
index 0000000000..33fa665f5b
--- /dev/null
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatchTask.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.aklivity.zilla.runtime.engine.internal.watcher;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import static org.agrona.LangUtil.rethrowUnchecked;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.WatchKey;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public abstract class EngineConfigWatchTask implements AutoCloseable, Callable
+{
+ private final Path configPath;
+ private final EngineConfigWatcher watcher;
+ private final ExecutorService executor;
+ private Map resourceKeys;
+
+ protected EngineConfigWatchTask(
+ Path configPath)
+ {
+ this.configPath = configPath;
+ this.watcher = new EngineConfigWatcher(configPath.getFileSystem());
+ this.executor = Executors.newScheduledThreadPool(2);
+ this.resourceKeys = new HashMap<>();
+ }
+
+ public void submit()
+ {
+ onPathChanged(configPath);
+ executor.submit(this);
+ }
+
+ public void watch(
+ String resource)
+ {
+ try
+ {
+ Path resourcePath = configPath.resolveSibling(resource);
+ WatchKey resourceKey = watcher.register(resourcePath, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
+ resourceKeys.put(resource, resourceKey);
+ }
+ catch (IOException ex)
+ {
+ rethrowUnchecked(ex);
+ }
+ }
+
+ public void unwatch(
+ String resource)
+ {
+ WatchKey resourceKey = resourceKeys.remove(resource);
+
+ if (resourceKey != null)
+ {
+ resourceKey.cancel();
+ }
+ }
+
+ @Override
+ public final Void call() throws IOException
+ {
+ watcher.register(configPath, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
+
+ while (true)
+ {
+ try
+ {
+ final WatchKey key = watcher.take();
+ final Path watchable = (Path) key.watchable();
+ onPathChanged(watchable);
+ }
+ catch (InterruptedException ex)
+ {
+ watcher.close();
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public final void close()
+ {
+ executor.shutdownNow();
+ }
+
+ protected abstract void onPathChanged(
+ Path watchedPath);
+}
diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatcher.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatcher.java
new file mode 100644
index 0000000000..2a66ffee7f
--- /dev/null
+++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/watcher/EngineConfigWatcher.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.aklivity.zilla.runtime.engine.internal.watcher;
+
+import static org.agrona.LangUtil.rethrowUnchecked;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchEvent.Modifier;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.agrona.LangUtil;
+
+public final class EngineConfigWatcher implements AutoCloseable
+{
+ private static final Function>> LOOKUP_RESOLVER;
+ static
+ {
+ Map>> resolvers = new HashMap<>();
+ resolvers.put("http", Set::of);
+
+ Function> defaultResolver = EngineConfigWatcher::resolveWatchables;
+ LOOKUP_RESOLVER = scheme -> resolvers.getOrDefault(scheme, defaultResolver);
+ }
+
+ private final Function> resolver;
+ private final WatchService watcher;
+ private final Map compoundKeys;
+
+ public EngineConfigWatcher(
+ FileSystem fileSystem)
+ {
+ this.resolver = LOOKUP_RESOLVER.apply(fileSystem.provider().getScheme());
+ this.watcher = newWatchService(fileSystem);
+ this.compoundKeys = new IdentityHashMap<>();
+ }
+
+ public WatchKey register(
+ Path watchable,
+ WatchEvent.Kind>... events) throws IOException
+ {
+ return register(watchable, events, new WatchEvent.Modifier[0]);
+ }
+
+ public WatchKey register(
+ Path watchable,
+ Kind>[] events,
+ Modifier... modifiers) throws IOException
+ {
+ WatchKey watchKey = null;
+
+ if (watcher != null)
+ {
+ watchKey = registerImpl(watchable, events, modifiers);
+ }
+
+ return watchKey;
+ }
+
+ public WatchKey take() throws InterruptedException
+ {
+ return watcher != null ? takeImpl() : null;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if (watcher != null)
+ {
+ watcher.close();
+ }
+ }
+
+ private WatchKey registerImpl(
+ Path watchable,
+ Kind>[] events,
+ Modifier... modifiers) throws IOException
+ {
+ Set watchPaths = resolver.apply(watchable);
+ Set watchKeys = new HashSet<>();
+
+ for (Path watchPath : watchPaths)
+ {
+ WatchKey registeredKey = watchPath.register(watcher, events, modifiers);
+ watchKeys.add(registeredKey);
+ }
+
+ CompoundWatchKey compoundKey = new CompoundWatchKey(watchable, watchKeys);
+ watchKeys.forEach(k -> compoundKeys.put(k, compoundKey));
+
+ return compoundKey;
+ }
+
+ private WatchKey takeImpl() throws InterruptedException
+ {
+ WatchKey watchKey = watcher.take();
+ CompoundWatchKey compoundKey = compoundKeys.get(watchKey);
+
+ return compoundKey;
+ }
+
+ private final class CompoundWatchKey implements WatchKey
+ {
+ private final Path watchable;
+ private final Set keys;
+ private final List> events;
+
+ CompoundWatchKey(
+ Path watchable,
+ Set keys)
+ {
+ this.watchable = watchable;
+ this.keys = keys;
+ this.events = new LinkedList<>();
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return keys.stream().allMatch(WatchKey::isValid);
+ }
+
+ @Override
+ public List> pollEvents()
+ {
+ List> events = this.events;
+
+ events.clear();
+ for (WatchKey key : keys)
+ {
+ // TODO filter watch events
+ events.addAll(key.pollEvents());
+ }
+
+ return events;
+ }
+
+ @Override
+ public boolean reset()
+ {
+ return keys.stream().allMatch(WatchKey::reset);
+ }
+
+ @Override
+ public void cancel()
+ {
+ keys.stream().forEach(WatchKey::cancel);
+ keys.forEach(compoundKeys::remove);
+ }
+
+ @Override
+ public Path watchable()
+ {
+ return watchable;
+ }
+ }
+
+ private static Set resolveWatchables(
+ Path watchable)
+ {
+ Set watchedPaths = new HashSet<>();
+
+ Deque observablePaths = new LinkedList<>();
+ observablePaths.addLast(watchable);
+
+ while (!observablePaths.isEmpty())
+ {
+ Path observablePath = observablePaths.removeFirst();
+
+ if (watchedPaths.add(observablePath))
+ {
+ if (Files.isSymbolicLink(observablePath))
+ {
+ Path targetPath = readSymbolicLink(observablePath);
+ targetPath = watchable.resolveSibling(targetPath).normalize();
+ observablePaths.addLast(targetPath);
+ }
+
+ for (Path ancestorPath = observablePath.getParent();
+ ancestorPath != null;
+ ancestorPath = ancestorPath.getParent())
+ {
+ if (Files.isSymbolicLink(ancestorPath))
+ {
+ if (watchedPaths.add(ancestorPath))
+ {
+ Path targetPath = readSymbolicLink(ancestorPath);
+ observablePaths.addLast(ancestorPath.resolve(targetPath).normalize());
+ }
+ }
+ }
+ }
+ }
+
+ Set watchables = new HashSet<>();
+ for (Path watchedPath : watchedPaths)
+ {
+ Path parentPath = watchedPath.getParent();
+ if (Files.exists(parentPath))
+ {
+ watchables.add(parentPath);
+ }
+ }
+
+ return watchables;
+ }
+
+ private static Path readSymbolicLink(
+ Path link)
+ {
+ Path target = null;
+
+ try
+ {
+ target = Files.readSymbolicLink(link);
+ }
+ catch (IOException ex)
+ {
+ LangUtil.rethrowUnchecked(ex);
+ }
+
+ return target;
+ }
+
+ private static WatchService newWatchService(
+ FileSystem fileSystem)
+ {
+ WatchService watcher = null;
+
+ try
+ {
+ watcher = fileSystem.newWatchService();
+ }
+ catch (UnsupportedOperationException ex)
+ {
+ // no watcher
+ }
+ catch (Exception ex)
+ {
+ rethrowUnchecked(ex);
+ }
+
+ return watcher;
+ }
+}
diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/internal/ReconfigureHttpIT.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/internal/ReconfigureHttpIT.java
index 37ced142fc..5abc52c836 100644
--- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/internal/ReconfigureHttpIT.java
+++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/internal/ReconfigureHttpIT.java
@@ -35,7 +35,6 @@
import io.aklivity.zilla.runtime.engine.test.annotation.Configuration;
import io.aklivity.zilla.runtime.engine.test.annotation.Configure;
-
public class ReconfigureHttpIT
{
public static final String ENGINE_CONFIG_POLL_INTERVAL_SECONDS = "zilla.engine.config.poll.interval.seconds";
@@ -68,58 +67,59 @@ public void setupRegisterLatch() throws Exception
}
@Test
- @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "1")
- @Configuration("http://localhost:8080/")
+ @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "0")
+ @Configuration("http://localhost:8080/zilla.yaml")
@Specification({
- "${app}/reconfigure.modify.via.http/server",
- "${net}/reconfigure.modify.via.http/client"
+ "${app}/reconfigure.create.via.http/server",
+ "${net}/reconfigure.create.via.http/client"
})
- public void shouldReconfigureWhenModifiedHttp() throws Exception
+ public void shouldReconfigureWhenCreatedViaHttp() throws Exception
{
k3po.start();
- k3po.awaitBarrier("CONNECTED");
EngineTest.TestEngineExt.registerLatch.await();
- k3po.notifyBarrier("CONFIG_CHANGED");
+ k3po.notifyBarrier("CONFIG_CREATED");
k3po.finish();
}
@Test
- @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "1")
- @Configuration("http://localhost:8080/")
+ @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "0")
+ @Configuration("http://localhost:8080/zilla.yaml")
@Specification({
- "${app}/reconfigure.create.via.http/server",
- "${net}/reconfigure.create.via.http/client"
+ "${app}/reconfigure.delete.via.http/server",
+ "${net}/reconfigure.delete.via.http/client"
})
- public void shouldReconfigureWhenCreatedHttp() throws Exception
+ public void shouldReconfigureWhenDeletedViaHttp() throws Exception
{
k3po.start();
EngineTest.TestEngineExt.registerLatch.await();
- k3po.notifyBarrier("CONFIG_CREATED");
+ k3po.notifyBarrier("CONFIG_DELETED");
k3po.finish();
}
@Test
- @Configuration("http://localhost:8080/")
+ @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "0")
+ @Configuration("http://localhost:8080/zilla.yaml")
@Specification({
- "${app}/reconfigure.delete.via.http/server",
- "${net}/reconfigure.delete.via.http/client"
+ "${app}/reconfigure.modify.via.http/server",
+ "${net}/reconfigure.modify.via.http/client"
})
- public void shouldReconfigureWhenDeletedHttp() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttp() throws Exception
{
k3po.start();
+ k3po.awaitBarrier("CONNECTED");
EngineTest.TestEngineExt.registerLatch.await();
- k3po.notifyBarrier("CONFIG_DELETED");
+ k3po.notifyBarrier("CONFIG_CHANGED");
k3po.finish();
}
@Test
- @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "1")
- @Configuration("http://localhost:8080/")
+ @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "0")
+ @Configuration("http://localhost:8080/zilla.yaml")
@Specification({
"${app}/reconfigure.modify.no.etag.via.http/server",
"${net}/reconfigure.modify.no.etag.via.http/client"
})
- public void shouldReconfigureWhenModifiedHttpEtagNotSupported() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttpWithNoEtag() throws Exception
{
k3po.start();
k3po.awaitBarrier("CONNECTED");
@@ -129,13 +129,13 @@ public void shouldReconfigureWhenModifiedHttpEtagNotSupported() throws Exception
}
@Test
- @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "1")
- @Configuration("http://localhost:8080/")
+ @Configure(name = ENGINE_CONFIG_POLL_INTERVAL_SECONDS, value = "0")
+ @Configuration("http://localhost:8080/zilla.yaml")
@Specification({
"${app}/reconfigure.server.error.via.http/server",
"${net}/reconfigure.server.error.via.http/client"
})
- public void shouldNotReconfigureWhenStatus500() throws Exception
+ public void shouldNotReconfigureViaHttpWhenServerError() throws Exception
{
k3po.start();
k3po.awaitBarrier("CHECK_RECONFIGURE");
diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/EngineRule.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/EngineRule.java
index 44278f08cf..3a455f474a 100644
--- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/EngineRule.java
+++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/EngineRule.java
@@ -31,11 +31,14 @@
import java.io.IOException;
import java.net.URI;
import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Properties;
import java.util.function.LongConsumer;
import java.util.function.LongSupplier;
@@ -73,7 +76,7 @@ public final class EngineRule implements TestRule
private Engine engine;
private EngineConfiguration configuration;
- private String configurationRoot;
+ private String configRoot;
private Predicate exceptions;
private boolean clean;
@@ -122,7 +125,7 @@ public EngineRule configure(
public EngineRule configurationRoot(
String configurationRoot)
{
- this.configurationRoot = configurationRoot;
+ this.configRoot = configurationRoot;
return this;
}
@@ -281,9 +284,9 @@ public Statement apply(
{
configure(ENGINE_CONFIG_URL, configURI.toURL());
}
- else if (configurationRoot != null)
+ else if (configRoot != null)
{
- String resourceName = String.format("%s/%s", configurationRoot, config.value());
+ String resourceName = String.format("%s/%s", configRoot, config.value());
URL configURL = testClass.getClassLoader().getResource(resourceName);
configure(ENGINE_CONFIG_URL, configURL);
}
@@ -309,7 +312,7 @@ else if (configurationRoot != null)
@Override
public void evaluate() throws Throwable
{
- EngineConfiguration config = configuration();
+ final EngineConfiguration config = configuration();
final Thread baseThread = Thread.currentThread();
final List errors = new ArrayList<>();
final ErrorHandler errorHandler = ex ->
@@ -318,6 +321,26 @@ public void evaluate() throws Throwable
errors.add(ex);
baseThread.interrupt();
};
+
+ FileSystem fs = null;
+
+ final URI configURI = config.configURI();
+
+ switch (configURI.getScheme())
+ {
+ case "jar":
+ String jarLocation = Path.of(
+ configURI.toString().replace("jar:file:", "").split("!")[0]
+ ).toUri().toString();
+ URI jarURI = new URI("jar", jarLocation, null);
+ fs = FileSystems.newFileSystem(jarURI, Map.of());
+ break;
+ case "http":
+ final String pollInterval = String.format("PT%dS", config.configPollIntervalSeconds());
+ fs = FileSystems.newFileSystem(configURI, Map.of("zilla.filesystem.http.poll.interval", pollInterval));
+ break;
+ }
+
engine = builder.config(config)
.errorHandler(errorHandler)
.build();
@@ -348,6 +371,10 @@ public void evaluate() throws Throwable
{
assertEmpty(errors);
}
+ if (fs != null)
+ {
+ fs.close();
+ }
}
}
}
diff --git a/runtime/filesystem-http/COPYRIGHT b/runtime/filesystem-http/COPYRIGHT
new file mode 100644
index 0000000000..0cb10b6f62
--- /dev/null
+++ b/runtime/filesystem-http/COPYRIGHT
@@ -0,0 +1,12 @@
+Copyright ${copyrightYears} Aklivity Inc
+
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
diff --git a/runtime/filesystem-http/LICENSE b/runtime/filesystem-http/LICENSE
new file mode 100644
index 0000000000..f6abb6327b
--- /dev/null
+++ b/runtime/filesystem-http/LICENSE
@@ -0,0 +1,114 @@
+ Aklivity Community License Agreement
+ Version 1.0
+
+This Aklivity Community License Agreement Version 1.0 (the “Agreement”) sets
+forth the terms on which Aklivity, Inc. (“Aklivity”) makes available certain
+software made available by Aklivity under this Agreement (the “Software”). BY
+INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE,
+YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO
+SUCH TERMS AND CONDITIONS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING
+THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU
+HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS
+AGREEMENT ON BEHALF OF SUCH ENTITY. “Licensee” means you, an individual, or
+the entity on whose behalf you are receiving the Software.
+
+ 1. LICENSE GRANT AND CONDITIONS.
+
+ 1.1 License. Subject to the terms and conditions of this Agreement,
+ Aklivity hereby grants to Licensee a non-exclusive, royalty-free,
+ worldwide, non-transferable, non-sublicenseable license during the term
+ of this Agreement to: (a) use the Software; (b) prepare modifications and
+ derivative works of the Software; (c) distribute the Software (including
+ without limitation in source code or object code form); and (d) reproduce
+ copies of the Software (the “License”). Licensee is not granted the
+ right to, and Licensee shall not, exercise the License for an Excluded
+ Purpose. For purposes of this Agreement, “Excluded Purpose” means making
+ available any software-as-a-service, platform-as-a-service,
+ infrastructure-as-a-service or other similar online service that competes
+ with Aklivity products or services that provide the Software.
+
+ 1.2 Conditions. In consideration of the License, Licensee’s distribution
+ of the Software is subject to the following conditions:
+
+ (a) Licensee must cause any Software modified by Licensee to carry
+ prominent notices stating that Licensee modified the Software.
+
+ (b) On each Software copy, Licensee shall reproduce and not remove or
+ alter all Aklivity or third party copyright or other proprietary
+ notices contained in the Software, and Licensee must provide the
+ notice below with each copy.
+
+ “This software is made available by Aklivity, Inc., under the
+ terms of the Aklivity Community License Agreement, Version 1.0
+ located at http://www.Aklivity.io/Aklivity-community-license. BY
+ INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF
+ THE SOFTWARE, YOU AGREE TO THE TERMS OF SUCH LICENSE AGREEMENT.”
+
+ 1.3 Licensee Modifications. Licensee may add its own copyright notices
+ to modifications made by Licensee and may provide additional or different
+ license terms and conditions for use, reproduction, or distribution of
+ Licensee’s modifications. While redistributing the Software or
+ modifications thereof, Licensee may choose to offer, for a fee or free of
+ charge, support, warranty, indemnity, or other obligations. Licensee, and
+ not Aklivity, will be responsible for any such obligations.
+
+ 1.4 No Sublicensing. The License does not include the right to
+ sublicense the Software, however, each recipient to which Licensee
+ provides the Software may exercise the Licenses so long as such recipient
+ agrees to the terms and conditions of this Agreement.
+
+ 2. TERM AND TERMINATION. This Agreement will continue unless and until
+ earlier terminated as set forth herein. If Licensee breaches any of its
+ conditions or obligations under this Agreement, this Agreement will
+ terminate automatically and the License will terminate automatically and
+ permanently.
+
+ 3. INTELLECTUAL PROPERTY. As between the parties, Aklivity will retain all
+ right, title, and interest in the Software, and all intellectual property
+ rights therein. Aklivity hereby reserves all rights not expressly granted
+ to Licensee in this Agreement. Aklivity hereby reserves all rights in its
+ trademarks and service marks, and no licenses therein are granted in this
+ Agreement.
+
+ 4. DISCLAIMER. Aklivity HEREBY DISCLAIMS ANY AND ALL WARRANTIES AND
+ CONDITIONS, EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, AND SPECIFICALLY
+ DISCLAIMS ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR
+ PURPOSE, WITH RESPECT TO THE SOFTWARE.
+
+ 5. LIMITATION OF LIABILITY. Aklivity WILL NOT BE LIABLE FOR ANY DAMAGES OF
+ ANY KIND, INCLUDING BUT NOT LIMITED TO, LOST PROFITS OR ANY CONSEQUENTIAL,
+ SPECIAL, INCIDENTAL, INDIRECT, OR DIRECT DAMAGES, HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, ARISING OUT OF THIS AGREEMENT. THE FOREGOING SHALL
+ APPLY TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+
+ 6.GENERAL.
+
+ 6.1 Governing Law. This Agreement will be governed by and interpreted in
+ accordance with the laws of the state of California, without reference to
+ its conflict of laws principles. If Licensee is located within the
+ United States, all disputes arising out of this Agreement are subject to
+ the exclusive jurisdiction of courts located in Santa Clara County,
+ California. USA. If Licensee is located outside of the United States,
+ any dispute, controversy or claim arising out of or relating to this
+ Agreement will be referred to and finally determined by arbitration in
+ accordance with the JAMS International Arbitration Rules. The tribunal
+ will consist of one arbitrator. The place of arbitration will be Palo
+ Alto, California. The language to be used in the arbitral proceedings
+ will be English. Judgment upon the award rendered by the arbitrator may
+ be entered in any court having jurisdiction thereof.
+
+ 6.2 Assignment. Licensee is not authorized to assign its rights under
+ this Agreement to any third party. Aklivity may freely assign its rights
+ under this Agreement to any third party.
+
+ 6.3 Other. This Agreement is the entire agreement between the parties
+ regarding the subject matter hereof. No amendment or modification of
+ this Agreement will be valid or binding upon the parties unless made in
+ writing and signed by the duly authorized representatives of both
+ parties. In the event that any provision, including without limitation
+ any condition, of this Agreement is held to be unenforceable, this
+ Agreement and all licenses and rights granted hereunder will immediately
+ terminate. Waiver by Aklivity of a breach of any provision of this
+ Agreement or the failure by Aklivity to exercise any right hereunder
+ will not be construed as a waiver of any subsequent breach of that right
+ or as a waiver of any other right.
\ No newline at end of file
diff --git a/runtime/filesystem-http/NOTICE b/runtime/filesystem-http/NOTICE
new file mode 100644
index 0000000000..d5dee9ccfc
--- /dev/null
+++ b/runtime/filesystem-http/NOTICE
@@ -0,0 +1,14 @@
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+This project includes:
+ agrona under The Apache License, Version 2.0
+
diff --git a/runtime/filesystem-http/NOTICE.template b/runtime/filesystem-http/NOTICE.template
new file mode 100644
index 0000000000..209ca12f74
--- /dev/null
+++ b/runtime/filesystem-http/NOTICE.template
@@ -0,0 +1,13 @@
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+This project includes:
+#GENERATED_NOTICES#
diff --git a/runtime/filesystem-http/mvnw b/runtime/filesystem-http/mvnw
new file mode 100755
index 0000000000..d2f0ea3808
--- /dev/null
+++ b/runtime/filesystem-http/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/runtime/filesystem-http/mvnw.cmd b/runtime/filesystem-http/mvnw.cmd
new file mode 100644
index 0000000000..b26ab24f03
--- /dev/null
+++ b/runtime/filesystem-http/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/runtime/filesystem-http/pom.xml b/runtime/filesystem-http/pom.xml
new file mode 100644
index 0000000000..f944c54a49
--- /dev/null
+++ b/runtime/filesystem-http/pom.xml
@@ -0,0 +1,205 @@
+
+
+
+ 4.0.0
+
+ io.aklivity.zilla
+ runtime
+ develop-SNAPSHOT
+ ../pom.xml
+
+
+ filesystem-http
+ zilla::runtime::filesystem-http
+
+
+
+ Aklivity Community License Agreement
+ https://www.aklivity.io/aklivity-community-license/
+ repo
+
+
+
+
+ 11
+ 11
+ 0.50
+ 0
+
+
+
+
+ ${project.groupId}
+ filesystem-http.spec
+ ${project.version}
+ provided
+
+
+ org.agrona
+ agrona
+
+
+ junit
+ junit
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+ com.vtence.hamcrest
+ hamcrest-jpa
+ test
+
+
+ com.github.npathai
+ hamcrest-optional
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ io.aklivity.k3po
+ control-junit
+ test
+
+
+ io.aklivity.k3po
+ lang
+ test
+
+
+ org.openjdk.jmh
+ jmh-core
+ test
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ test
+
+
+
+
+
+
+ org.jasig.maven
+ maven-notice-plugin
+
+
+ com.mycila
+ license-maven-plugin
+
+
+ maven-checkstyle-plugin
+
+
+ maven-dependency-plugin
+
+
+ unpack-test-resources
+ process-test-resources
+
+ unpack
+
+
+
+
+ ${project.groupId}
+ filesystem-http.spec
+
+
+ ^\Qio/aklivity/zilla/specs/filesystem/http/\E
+ io/aklivity/zilla/runtime/filesystem/http/internal/
+
+
+
+
+
+ io/aklivity/zilla/specs/filesystem/http/application/**/*,
+
+ ${project.build.directory}/test-classes
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.moditect
+ moditect-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+ io.aklivity.k3po
+ k3po-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ ${jacoco.coverage.ratio}
+
+
+ CLASS
+ MISSEDCOUNT
+ ${jacoco.missed.count}
+
+
+
+
+
+
+
+ io.gatling
+ maven-shade-plugin
+
+
+
+ org.openjdk.jmh:jmh-core
+ net.sf.jopt-simple:jopt-simple
+ org.apache.commons:commons-math3
+ commons-cli:commons-cli
+ com.github.biboudis:jmh-profilers
+
+
+
+
+
+
+
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystem.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystem.java
new file mode 100644
index 0000000000..8f274fd89d
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystem.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import static java.net.http.HttpClient.Redirect.NORMAL;
+import static java.net.http.HttpClient.Version.HTTP_2;
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.Map;
+import java.util.Set;
+
+public final class HttpFileSystem extends FileSystem
+{
+ private static final String HTTP_PATH_SEPARATOR = "/";
+
+ private final HttpFileSystemProvider provider;
+ private final URI root;
+ private final HttpFileSystemConfiguration config;
+ private final HttpClient client;
+
+ HttpFileSystem(
+ HttpFileSystemProvider provider,
+ URI root,
+ Map env)
+ {
+ this.provider = provider;
+ this.root = root;
+ this.config = new HttpFileSystemConfiguration(env);
+ this.client = HttpClient.newBuilder()
+ .version(HTTP_2)
+ .followRedirects(NORMAL)
+ .build();
+ }
+
+ @Override
+ public HttpFileSystemProvider provider()
+ {
+ return provider;
+ }
+
+ @Override
+ public void close()
+ {
+ provider.closeFileSystem(root);
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isReadOnly()
+ {
+ return true;
+ }
+
+ @Override
+ public String getSeparator()
+ {
+ return HTTP_PATH_SEPARATOR;
+ }
+
+ @Override
+ public Iterable getRootDirectories()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Iterable getFileStores()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Set supportedFileAttributeViews()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path getPath(
+ String first,
+ String... more)
+ {
+ requireNonNull(first);
+ requireNonNull(more);
+
+ String path = more.length > 0
+ ? first + HTTP_PATH_SEPARATOR + String.join(HTTP_PATH_SEPARATOR, more)
+ : first;
+
+ return getPath(URI.create(path));
+ }
+
+ public HttpPath getPath(
+ URI uri)
+ {
+ return new HttpPath(this, uri);
+ }
+
+ @Override
+ public PathMatcher getPathMatcher(
+ String syntaxAndPattern)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public UserPrincipalLookupService getUserPrincipalLookupService()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public HttpWatchService newWatchService()
+ {
+ return new HttpWatchService(config);
+ }
+
+ HttpClient client()
+ {
+ return client;
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemConfiguration.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemConfiguration.java
new file mode 100644
index 0000000000..c9757554db
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+
+public final class HttpFileSystemConfiguration
+{
+ public static final String POLL_INTERVAL_PROPERTY_NAME = "zilla.filesystem.http.poll.interval";
+
+ private static final Duration POLL_INTERVAL_PROPERTY_DEFAULT = Duration.parse("PT30S");
+
+ private final Map env;
+
+ HttpFileSystemConfiguration(
+ Map env)
+ {
+ this.env = Objects.requireNonNull(env);
+ }
+
+ public Duration pollInterval()
+ {
+ String value = env != null ? (String) env.get(POLL_INTERVAL_PROPERTY_NAME) : null;
+ return value != null ? Duration.parse(value) : POLL_INTERVAL_PROPERTY_DEFAULT;
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemProvider.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemProvider.java
new file mode 100644
index 0000000000..14587c81f9
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpFileSystemProvider.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+
+public class HttpFileSystemProvider extends FileSystemProvider
+{
+ private final Map fileSystems = new ConcurrentHashMap<>();
+
+ @Override
+ public String getScheme()
+ {
+ return "http";
+ }
+
+ @Override
+ @SuppressWarnings("resource")
+ public FileSystem newFileSystem(
+ URI uri,
+ Map env)
+ {
+ checkURI(uri);
+
+ URI rootURI = uri.resolve("/");
+ return fileSystems.compute(rootURI, (u, fs) -> computeHttpFileSystem(u, fs, env));
+ }
+
+ @Override
+ public FileSystem getFileSystem(
+ URI uri)
+ {
+ checkURI(uri);
+ URI rootURI = uri.resolve("/");
+ HttpFileSystem hfs = fileSystems.get(rootURI);
+ if (hfs == null)
+ {
+ throw new FileSystemNotFoundException();
+ }
+ return hfs;
+ }
+
+ @Override
+ public Path getPath(
+ URI uri)
+ {
+ checkURI(uri);
+ URI rootURI = uri.resolve("/");
+ HttpFileSystem hfs = fileSystems.computeIfAbsent(rootURI, this::newHttpFileSystem);
+ return hfs.getPath(uri);
+ }
+
+ @Override
+ public FileSystem newFileSystem(
+ Path path,
+ Map env)
+ {
+ return newFileSystem(path.toUri(), env);
+ }
+
+ @Override
+ public InputStream newInputStream(
+ Path path,
+ OpenOption... options)
+ {
+ HttpPath httpPath = checkPath(path);
+ return new ByteArrayInputStream(httpPath.readBody());
+ }
+
+ @Override
+ public OutputStream newOutputStream(
+ Path path,
+ OpenOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public FileChannel newFileChannel(
+ Path path,
+ Set extends OpenOption> options,
+ FileAttribute>... attrs)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public AsynchronousFileChannel newAsynchronousFileChannel(
+ Path path,
+ Set extends OpenOption> options,
+ ExecutorService executor,
+ FileAttribute>... attrs)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public SeekableByteChannel newByteChannel(
+ Path path,
+ Set extends OpenOption> options,
+ FileAttribute>... attrs)
+ {
+ HttpPath httpPath = checkPath(path);
+ return new ReadOnlyByteArrayChannel(httpPath.readBody());
+ }
+
+ @Override
+ public DirectoryStream newDirectoryStream(
+ Path dir,
+ DirectoryStream.Filter super Path> filter)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void createDirectory(
+ Path dir,
+ FileAttribute>... attrs)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void createSymbolicLink(
+ Path link,
+ Path target, FileAttribute>... attrs)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void createLink(
+ Path link,
+ Path existing)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void delete(
+ Path path)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean deleteIfExists(
+ Path path)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path readSymbolicLink(
+ Path link)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void copy(
+ Path source,
+ Path target,
+ CopyOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void move(
+ Path source,
+ Path target,
+ CopyOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isSameFile(
+ Path path,
+ Path path2)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isHidden(
+ Path path)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public FileStore getFileStore(
+ Path path)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void checkAccess(
+ Path path,
+ AccessMode... modes)
+ {
+ Objects.requireNonNull(path);
+ }
+
+ @Override
+ public V getFileAttributeView(
+ Path path,
+ Class type,
+ LinkOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public A readAttributes(
+ Path path,
+ Class type,
+ LinkOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Map readAttributes(
+ Path path,
+ String attributes,
+ LinkOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void setAttribute(
+ Path path,
+ String attribute,
+ Object value, LinkOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ void closeFileSystem(
+ URI uri)
+ {
+ fileSystems.remove(uri);
+ }
+
+ private void checkURI(
+ URI uri)
+ {
+ if (!uri.getScheme().equalsIgnoreCase(getScheme()))
+ {
+ throw new IllegalArgumentException("URI does not match this provider");
+ }
+
+ if (uri.getPath() == null)
+ {
+ throw new IllegalArgumentException("Path component is undefined");
+ }
+ }
+
+ private HttpPath checkPath(
+ Path path)
+ {
+ if (!path.getFileSystem().provider().getScheme().equalsIgnoreCase(getScheme()))
+ {
+ throw new IllegalArgumentException("Scheme does not match this provider");
+ }
+ return HttpPath.class.cast(path);
+ }
+
+ private HttpFileSystem computeHttpFileSystem(
+ URI uri,
+ HttpFileSystem hfs,
+ Map env)
+ {
+ if (hfs != null)
+ {
+ throw new FileSystemAlreadyExistsException();
+ }
+
+ return new HttpFileSystem(this, uri, env);
+ }
+
+ private HttpFileSystem newHttpFileSystem(
+ URI uri)
+ {
+ return new HttpFileSystem(this, uri, Map.of());
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpPath.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpPath.java
new file mode 100644
index 0000000000..bb6d54c838
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpPath.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Objects;
+
+public final class HttpPath implements Path
+{
+ private static final byte[] EMPTY_BODY = new byte[0];
+
+ private final HttpFileSystem fs;
+ private final URI location;
+
+ private volatile byte[] body;
+ private volatile String etag;
+
+ private volatile int changeCount;
+ private volatile int readCount;
+
+ HttpPath(
+ HttpFileSystem fs,
+ URI location)
+ {
+ this.fs = Objects.requireNonNull(fs);
+ this.location = Objects.requireNonNull(location);
+ }
+
+ @Override
+ public HttpFileSystem getFileSystem()
+ {
+ return fs;
+ }
+
+ @Override
+ public boolean isAbsolute()
+ {
+ return true;
+ }
+
+ @Override
+ public Path getRoot()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path getFileName()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path getParent()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public int getNameCount()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path getName(
+ int index)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path subpath(
+ int beginIndex,
+ int endIndex)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean startsWith(
+ Path other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean startsWith(
+ String other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean endsWith(
+ Path other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean endsWith(
+ String other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path normalize()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path resolve(
+ Path other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path resolve(
+ String other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path resolveSibling(
+ Path other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public Path resolveSibling(
+ String other)
+ {
+ return new HttpPath(fs, location.resolve(URI.create(other)));
+ }
+
+ @Override
+ public Path relativize(
+ Path other)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public URI toUri()
+ {
+ return location;
+ }
+
+ @Override
+ public Path toAbsolutePath()
+ {
+ return this;
+ }
+
+ @Override
+ public Path toRealPath(
+ LinkOption... options)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public File toFile()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public WatchKey register(
+ WatchService watcher,
+ WatchEvent.Kind>... events) throws IOException
+ {
+ return register(watcher, events, new WatchEvent.Modifier[0]);
+ }
+
+ @Override
+ public WatchKey register(
+ WatchService watcher,
+ WatchEvent.Kind>[] events,
+ WatchEvent.Modifier... modifiers)
+ throws IOException
+ {
+ HttpWatchService httpWatcher = checkWatcher(watcher);
+ return httpWatcher.register(this, events, modifiers);
+ }
+
+ @Override
+ public Iterator iterator()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public int compareTo(
+ Path other)
+ {
+ HttpPath that = (HttpPath) other;
+
+ return location.compareTo(that.location);
+ }
+
+ @Override
+ public String toString()
+ {
+ return location.toString();
+ }
+
+ @Override
+ public boolean equals(
+ Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+
+ HttpPath path = (HttpPath) o;
+ return Objects.equals(location, path.location);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(location);
+ }
+
+ byte[] readBody()
+ {
+ if (readCount == changeCount)
+ {
+ try
+ {
+ HttpClient client = fs.client();
+ HttpRequest request = newReadRequest();
+ HttpResponse response = client.send(request, BodyHandlers.ofByteArray());
+ success(response);
+ }
+ catch (Exception ex)
+ {
+ failure(ex);
+ }
+ }
+
+ readCount = changeCount;
+
+ return body;
+ }
+
+ void success(
+ HttpResponse response)
+ {
+ final int status = response.statusCode();
+
+ switch (status)
+ {
+ case HTTP_OK:
+ case HTTP_NO_CONTENT:
+ byte[] oldBody = body;
+ body = status == HTTP_NO_CONTENT ? EMPTY_BODY : response.body();
+ etag = response.headers().firstValue("Etag").orElse(null);
+ if (body == null ||
+ oldBody == null ||
+ !Arrays.equals(body, oldBody))
+ {
+ changeCount++;
+ }
+ break;
+ case HTTP_NOT_FOUND:
+ body = EMPTY_BODY;
+ etag = null;
+ changeCount++;
+ break;
+ case HTTP_NOT_MODIFIED:
+ break;
+ }
+ }
+
+ Void failure(
+ Throwable ex)
+ {
+ body = HttpPath.EMPTY_BODY;
+ etag = null;
+ changeCount++;
+ return null;
+ }
+
+ int changeCount()
+ {
+ return changeCount;
+ }
+
+ boolean exists()
+ {
+ return body != null;
+ }
+
+ private HttpRequest newReadRequest()
+ {
+ HttpRequest.Builder request = HttpRequest.newBuilder()
+ .GET()
+ .uri(location);
+
+ if (etag != null && !etag.isEmpty())
+ {
+ request = request.headers("If-None-Match", etag);
+ }
+
+ return request.build();
+ }
+
+ HttpRequest newWatchRequest()
+ {
+ HttpRequest.Builder request = HttpRequest.newBuilder()
+ .GET()
+ .uri(location);
+
+ if (etag != null)
+ {
+ request = request.headers("If-None-Match", etag, "Prefer", "wait=86400");
+ }
+
+ return request.build();
+ }
+
+ private HttpWatchService checkWatcher(
+ WatchService watcher)
+ {
+ requireNonNull(watcher);
+
+ if (!(watcher instanceof HttpWatchService))
+ {
+ throw new ProviderMismatchException();
+ }
+
+ return (HttpWatchService) watcher;
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpWatchService.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpWatchService.java
new file mode 100644
index 0000000000..5c52e1cbde
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpWatchService.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import static java.lang.System.currentTimeMillis;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.agrona.LangUtil.rethrowUnchecked;
+
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public final class HttpWatchService implements WatchService
+{
+ private final WatchKey closeKey = new HttpWatchKey();
+
+ private final Duration pollInterval;
+ private final Collection watchKeys;
+ private final BlockingQueue pendingKeys;
+ private final ScheduledExecutorService executor;
+
+ private volatile boolean closed;
+
+ HttpWatchService(
+ HttpFileSystemConfiguration config)
+ {
+ this.pollInterval = config.pollInterval();
+ this.watchKeys = new ConcurrentSkipListSet<>();
+ this.pendingKeys = new LinkedBlockingQueue<>();
+ this.executor = Executors.newScheduledThreadPool(2);
+ }
+
+ @Override
+ public void close()
+ {
+ watchKeys.forEach(HttpWatchKey::cancel);
+ watchKeys.clear();
+
+ closed = true;
+ pendingKeys.clear();
+ pendingKeys.offer(closeKey);
+
+ executor.shutdownNow();
+
+ try
+ {
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException ex)
+ {
+ rethrowUnchecked(ex);
+ }
+ }
+
+ @Override
+ public WatchKey poll()
+ {
+ checkOpen();
+ WatchKey key = pendingKeys.poll();
+ return key != closeKey ? key : null;
+ }
+
+ @Override
+ public WatchKey poll(
+ long timeout,
+ TimeUnit unit) throws InterruptedException
+ {
+ checkOpen();
+ WatchKey key = pendingKeys.poll(timeout, unit);
+ return key != closeKey ? key : null;
+ }
+
+ @Override
+ public WatchKey take() throws InterruptedException
+ {
+ checkOpen();
+ WatchKey key = pendingKeys.take();
+ return key != closeKey ? key : null;
+ }
+
+ HttpWatchKey register(
+ HttpPath path,
+ WatchEvent.Kind>[] events,
+ WatchEvent.Modifier... modifiers)
+ {
+ checkEvents(events);
+ checkModifiers(modifiers);
+
+ HttpWatchKey watchKey = new HttpWatchKey(this, path);
+ watchKeys.add(watchKey);
+ watchKey.watch();
+
+ return watchKey;
+ }
+
+ private void checkOpen()
+ {
+ if (closed)
+ {
+ throw new ClosedWatchServiceException();
+ }
+ }
+
+ private void checkEvents(
+ WatchEvent.Kind>[] events)
+ {
+ for (WatchEvent.Kind> event : events)
+ {
+ if (!event.equals(ENTRY_CREATE) &&
+ !event.equals(ENTRY_MODIFY) &&
+ !event.equals(ENTRY_DELETE))
+ {
+ throw new IllegalArgumentException(String.format("%s event kind not supported", event));
+ }
+ }
+ }
+
+ private void checkModifiers(
+ WatchEvent.Modifier[] modifiers)
+ {
+ if (modifiers.length > 0)
+ {
+ throw new IllegalArgumentException("Modifiers are not supported");
+ }
+ }
+
+ private void watchBody(
+ HttpWatchKey watchKey)
+ {
+ long elapsed = currentTimeMillis() - watchKey.lastWatchAt;
+ long delay = Math.max(SECONDS.toMillis(pollInterval.getSeconds()) - elapsed, 0L);
+
+ executor.schedule(watchKey::watchBody, delay, MILLISECONDS);
+ }
+
+ private void signalKey(
+ HttpWatchKey watchKey)
+ {
+ pendingKeys.offer(watchKey);
+ }
+
+ private void cancelKey(
+ HttpWatchKey watchKey)
+ {
+ watchKeys.remove(watchKey);
+ }
+
+ private static final class HttpWatchKey implements WatchKey, Comparable
+ {
+ private final HttpWatchService watcher;
+ private final HttpPath path;
+
+ private List> watchEvents = Collections.synchronizedList(new LinkedList<>());
+
+ private volatile boolean valid;
+ private volatile CompletableFuture future;
+ private long lastWatchAt;
+
+ private HttpWatchKey()
+ {
+ this.watcher = null;
+ this.path = null;
+ this.valid = false;
+ }
+
+ private HttpWatchKey(
+ HttpWatchService watcher,
+ HttpPath path)
+ {
+ this.watcher = watcher;
+ this.path = path;
+ this.valid = true;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return valid;
+ }
+
+ @Override
+ public List> pollEvents()
+ {
+ List> result = watchEvents;
+ watchEvents = Collections.synchronizedList(new LinkedList<>());
+ return result;
+ }
+
+ @Override
+ public boolean reset()
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public void cancel()
+ {
+ future.cancel(true);
+ watcher.cancelKey(this);
+ valid = false;
+ }
+
+ @Override
+ public HttpPath watchable()
+ {
+ return path;
+ }
+
+ @Override
+ public boolean equals(
+ Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+
+ HttpWatchKey that = (HttpWatchKey) o;
+ return Objects.equals(path, that.path);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(path);
+ }
+
+ @Override
+ public int compareTo(
+ HttpWatchKey that)
+ {
+ return path.compareTo(that.path);
+ }
+
+ private void watch()
+ {
+ if (valid)
+ {
+ watcher.watchBody(this);
+ }
+ }
+
+ private void watchBody()
+ {
+ HttpClient client = path.getFileSystem().client();
+ HttpRequest request = path.newWatchRequest();
+
+ this.lastWatchAt = currentTimeMillis();
+
+ this.future = client.sendAsync(request, BodyHandlers.ofByteArray())
+ .thenAccept(this::success)
+ .exceptionally(this::failure);
+ }
+
+ private void success(
+ HttpResponse response)
+ {
+ int changeCount = path.changeCount();
+ boolean exists = path.exists();
+
+ path.success(response);
+
+ if (path.changeCount() != changeCount)
+ {
+ if (exists == path.exists())
+ {
+ signalEvent(ENTRY_MODIFY);
+ }
+ else if (exists)
+ {
+ signalEvent(ENTRY_DELETE);
+ }
+ else
+ {
+ signalEvent(ENTRY_CREATE);
+ }
+
+ this.lastWatchAt = 0L;
+ }
+
+ watcher.watchBody(this);
+ }
+
+ private Void failure(
+ Throwable ex)
+ {
+ int changeCount = path.changeCount();
+ boolean exists = path.exists();
+
+ path.failure(ex);
+
+ if (path.changeCount() != changeCount)
+ {
+ if (exists == path.exists())
+ {
+ signalEvent(ENTRY_MODIFY);
+ }
+ else if (exists)
+ {
+ signalEvent(ENTRY_DELETE);
+ }
+ else
+ {
+ signalEvent(ENTRY_CREATE);
+ }
+ }
+
+ // (back off)?
+ watcher.watchBody(this);
+
+ return null;
+ }
+
+ private void signalEvent(
+ WatchEvent.Kind kind)
+ {
+ watchEvents.add(new HttpWatchEvent(kind, path));
+ watcher.signalKey(this);
+ }
+
+ private static class HttpWatchEvent implements WatchEvent
+ {
+ private final WatchEvent.Kind kind;
+ private final Path context;
+ private final int count;
+
+ HttpWatchEvent(
+ WatchEvent.Kind type,
+ Path context)
+ {
+ this.kind = type;
+ this.context = context;
+ this.count = 1;
+ }
+
+ @Override
+ public WatchEvent.Kind kind()
+ {
+ return kind;
+ }
+
+ @Override
+ public Path context()
+ {
+ return context;
+ }
+
+ @Override
+ public int count()
+ {
+ return count;
+ }
+ }
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpsFileSystemProvider.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpsFileSystemProvider.java
new file mode 100644
index 0000000000..156d88ef35
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/HttpsFileSystemProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+public class HttpsFileSystemProvider extends HttpFileSystemProvider
+{
+ @Override
+ public String getScheme()
+ {
+ return "https";
+ }
+}
diff --git a/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/ReadOnlyByteArrayChannel.java b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/ReadOnlyByteArrayChannel.java
new file mode 100644
index 0000000000..b18bc64c1a
--- /dev/null
+++ b/runtime/filesystem-http/src/main/java/io/aklivity/zilla/runtime/filesystem/http/internal/ReadOnlyByteArrayChannel.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http.internal;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SeekableByteChannel;
+
+final class ReadOnlyByteArrayChannel implements SeekableByteChannel
+{
+ private final byte[] data;
+ private int position;
+ private boolean closed;
+
+ ReadOnlyByteArrayChannel(
+ byte[] data)
+ {
+ this.data = data;
+ this.position = 0;
+ this.closed = false;
+ }
+
+ @Override
+ public int read(
+ ByteBuffer dst) throws IOException
+ {
+ ensureOpen();
+ int bytesRead;
+ if (position >= data.length)
+ {
+ bytesRead = -1;
+ }
+ else
+ {
+ bytesRead = Math.min(dst.remaining(), data.length - position);
+ dst.put(data, position, bytesRead);
+ position += bytesRead;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public int write(
+ ByteBuffer src)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public long position() throws IOException
+ {
+ ensureOpen();
+ return position;
+ }
+
+ @Override
+ public SeekableByteChannel position(
+ long newPosition) throws IOException
+ {
+ ensureOpen();
+ if (newPosition < 0 || newPosition > data.length)
+ {
+ throw new IllegalArgumentException("Position out of bounds");
+ }
+ this.position = (int) newPosition;
+ return this;
+ }
+
+ @Override
+ public long size() throws IOException
+ {
+ ensureOpen();
+ return data.length;
+ }
+
+ @Override
+ public SeekableByteChannel truncate(
+ long size)
+ {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return !closed;
+ }
+
+ @Override
+ public void close()
+ {
+ closed = true;
+ }
+
+ private void ensureOpen() throws IOException
+ {
+ if (closed)
+ {
+ throw new ClosedChannelException();
+ }
+ }
+}
diff --git a/runtime/filesystem-http/src/main/moditect/module-info.java b/runtime/filesystem-http/src/main/moditect/module-info.java
new file mode 100644
index 0000000000..a953179fba
--- /dev/null
+++ b/runtime/filesystem-http/src/main/moditect/module-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+module io.aklivity.zilla.runtime.filesystem.http
+{
+ requires java.net.http;
+ requires org.agrona.core;
+
+ provides java.nio.file.spi.FileSystemProvider with
+ io.aklivity.zilla.runtime.filesystem.http.internal.HttpFileSystemProvider,
+ io.aklivity.zilla.runtime.filesystem.http.internal.HttpsFileSystemProvider;
+}
diff --git a/runtime/filesystem-http/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/runtime/filesystem-http/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider
new file mode 100644
index 0000000000..0801cb9df9
--- /dev/null
+++ b/runtime/filesystem-http/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider
@@ -0,0 +1,2 @@
+io.aklivity.zilla.runtime.filesystem.http.internal.HttpFileSystemProvider
+io.aklivity.zilla.runtime.filesystem.http.internal.HttpsFileSystemProvider
diff --git a/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemIT.java b/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemIT.java
new file mode 100644
index 0000000000..6fef361311
--- /dev/null
+++ b/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemIT.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http;
+
+import static io.aklivity.zilla.runtime.filesystem.http.internal.HttpFileSystemConfiguration.POLL_INTERVAL_PROPERTY_NAME;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.rules.RuleChain.outerRule;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import io.aklivity.k3po.runtime.junit.annotation.Specification;
+import io.aklivity.k3po.runtime.junit.rules.K3poRule;
+
+public class HttpFileSystemIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("app", "io/aklivity/zilla/specs/filesystem/http/application");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${app}/read.success/server",
+ })
+ public void shouldReadString() throws Exception
+ {
+ // GIVEN
+ URI helloURI = URI.create("http://localhost:8080/hello.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(helloURI, Map.of()))
+ {
+ Path helloPath = fs.getPath(helloURI.toString());
+
+ // WHEN
+ k3po.start();
+ String helloBody = Files.readString(helloPath);
+ k3po.finish();
+
+ // THEN
+ assertThat(helloBody, equalTo("Hello World!"));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.success.etag.not.modified/server",
+ })
+ public void shouldReadStringEtagNotModified() throws Exception
+ {
+ // GIVEN
+ URI helloURI = URI.create("http://localhost:8080/hello.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(helloURI, Map.of()))
+ {
+ Path helloPath = fs.getPath(helloURI.toString());
+
+ // WHEN
+ k3po.start();
+ String helloBody1 = Files.readString(helloPath);
+ String helloBody2 = Files.readString(helloPath);
+ k3po.finish();
+
+ // THEN
+ assertThat(helloBody1, equalTo("Hello World!"));
+ assertThat(helloBody2, equalTo("Hello World!"));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.success.etag.modified/server",
+ })
+ public void shouldReadStringEtagModified() throws Exception
+ {
+ // GIVEN
+ URI helloURI = URI.create("http://localhost:8080/hello.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(helloURI, Map.of()))
+ {
+ Path helloPath = fs.getPath(helloURI.toString());
+
+ // WHEN
+ k3po.start();
+ String helloBody1 = Files.readString(helloPath);
+ String helloBody2 = Files.readString(helloPath);
+ k3po.finish();
+
+ // THEN
+ assertThat(helloBody1, equalTo("Hello World!"));
+ assertThat(helloBody2, equalTo("Hello Universe!"));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.notfound/server",
+ })
+ public void shouldReadStringNotFound() throws Exception
+ {
+ // GIVEN
+ URI notFoundURI = URI.create("http://localhost:8080/notfound.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(notFoundURI, Map.of()))
+ {
+ Path notFoundPath = fs.getPath(notFoundURI.toString());
+
+ // WHEN
+ k3po.start();
+ String notFoundBody = Files.readString(notFoundPath);
+ k3po.finish();
+
+ // THEN
+ assertThat(notFoundBody, equalTo(""));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.notfound.success/server",
+ })
+ public void shouldReadStringNotFoundSuccess() throws Exception
+ {
+ // GIVEN
+ URI helloURI = URI.create("http://localhost:8080/hello.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(helloURI, Map.of()))
+ {
+ Path helloPath = fs.getPath(helloURI.toString());
+
+ // WHEN
+ k3po.start();
+ String helloBody1 = Files.readString(helloPath);
+ String helloBody2 = Files.readString(helloPath);
+ k3po.finish();
+
+ // THEN
+ assertThat(helloBody1, equalTo(""));
+ assertThat(helloBody2, equalTo("Hello World!"));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.success/server",
+ })
+ public void shouldReadInputStream() throws Exception
+ {
+ // GIVEN
+ URI helloURI = URI.create("http://localhost:8080/hello.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(helloURI, Map.of()))
+ {
+ Path helloPath = fs.getPath(helloURI.toString());
+
+ // WHEN
+ k3po.start();
+ InputStream helloIs = Files.newInputStream(helloPath);
+ String helloBody = new String(helloIs.readAllBytes());
+ helloIs.close();
+ k3po.finish();
+
+ // THEN
+ assertThat(helloBody, equalTo("Hello World!"));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.notfound/server",
+ })
+ public void shouldReadInputStreamNotFound() throws Exception
+ {
+ // GIVEN
+ URI notFoundURI = URI.create("http://localhost:8080/notfound.txt");
+ try (FileSystem fs = FileSystems.newFileSystem(notFoundURI, Map.of()))
+ {
+ Path notFoundPath = fs.getPath(notFoundURI.toString());
+
+ // WHEN
+ k3po.start();
+ InputStream notFoundIs = Files.newInputStream(notFoundPath);
+ String notFoundBody = new String(notFoundIs.readAllBytes());
+ notFoundIs.close();
+ k3po.finish();
+
+ // THEN
+ assertThat(notFoundBody, equalTo(""));
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/watch/server",
+ })
+ public void shouldWatch() throws Exception
+ {
+ // GIVEN
+ URI uri = URI.create("http://localhost:8080/hello.txt");
+ Map env = Map.of(POLL_INTERVAL_PROPERTY_NAME, "PT0S");
+ try (FileSystem fs = FileSystems.newFileSystem(uri, env))
+ {
+ Path path = fs.getPath(uri.toString());
+
+ try (WatchService watcher = fs.newWatchService())
+ {
+ // WHEN
+ k3po.start();
+
+ k3po.notifyBarrier("REGISTERED");
+ path.register(watcher);
+
+ WatchKey key1 = watcher.take();
+ List> events1 = key1.pollEvents();
+
+ k3po.notifyBarrier("MODIFIED");
+
+ WatchKey key2 = watcher.take();
+ List> events2 = key2.pollEvents();
+
+ k3po.finish();
+
+ // THEN
+ assertThat(events1.size(), equalTo(1));
+ assertThat(events1.get(0).kind(), equalTo(ENTRY_CREATE));
+ assertThat(events1.get(0).context(), equalTo(path));
+ assertThat(events2.size(), equalTo(1));
+ assertThat(events2.get(0).kind(), equalTo(ENTRY_MODIFY));
+ assertThat(events2.get(0).context(), equalTo(path));
+ }
+ }
+ }
+
+ @Test
+ @Specification({
+ "${app}/watch.read/server",
+ })
+ public void shouldWatchRead() throws Exception
+ {
+ // GIVEN
+ URI uri = URI.create("http://localhost:8080/hello.txt");
+ Map env = Map.of(POLL_INTERVAL_PROPERTY_NAME, "PT0S");
+ try (FileSystem fs = FileSystems.newFileSystem(uri, env))
+ {
+ Path path = fs.getPath(uri.toString());
+
+ try (WatchService watcher = fs.newWatchService())
+ {
+ // WHEN
+ k3po.start();
+
+ String body1 = Files.readString(path);
+
+ path.register(watcher);
+
+ watcher.take();
+
+ String body2 = Files.readString(path);
+
+ k3po.finish();
+
+ // THEN
+ assertThat(body1, equalTo("Hello World!"));
+ assertThat(body2, equalTo("Hello Universe!"));
+ }
+ }
+ }
+}
diff --git a/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemTest.java b/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemTest.java
new file mode 100644
index 0000000000..2682fafc56
--- /dev/null
+++ b/runtime/filesystem-http/src/test/java/io/aklivity/zilla/runtime/filesystem/http/HttpFileSystemTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.runtime.filesystem.http;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+import org.junit.Test;
+
+public class HttpFileSystemTest
+{
+ @Test
+ public void testHttpPath() throws Exception
+ {
+ // GIVEN
+ String httpUrl = "http://localhost:4242/hello.txt";
+
+ // WHEN
+ Path path = Path.of(new URI(httpUrl));
+
+ // THEN
+ assertThat(path.getFileSystem().getClass().getSimpleName(), equalTo("HttpFileSystem"));
+ assertThat(path.getFileSystem().provider().getClass().getSimpleName(), equalTo("HttpFileSystemProvider"));
+ assertThat(path.getFileSystem().provider().getScheme(), equalTo("http"));
+ }
+
+ @Test
+ public void testHttpsPath() throws Exception
+ {
+ // GIVEN
+ String httpsUrl = "https://localhost:4242/hello.txt";
+
+ // WHEN
+ Path path = Path.of(new URI(httpsUrl));
+
+ // THEN
+ assertThat(path.getFileSystem().getClass().getSimpleName(), equalTo("HttpFileSystem"));
+ assertThat(path.getFileSystem().provider().getClass().getSimpleName(), equalTo("HttpsFileSystemProvider"));
+ assertThat(path.getFileSystem().provider().getScheme(), equalTo("https"));
+ }
+
+ @Test
+ public void testHttpSiblingString() throws Exception
+ {
+ // GIVEN
+ String httpUrl = "http://localhost:4242/greeting/hello.txt";
+ Path path = Path.of(new URI(httpUrl));
+
+ // WHEN
+ Path sibling = path.resolveSibling("bye.txt");
+
+ // THEN
+ assertThat(sibling.getFileSystem().getClass().getSimpleName(), equalTo("HttpFileSystem"));
+ assertThat(sibling.getFileSystem().provider().getClass().getSimpleName(), equalTo("HttpFileSystemProvider"));
+ assertThat(sibling.toString(), equalTo("http://localhost:4242/greeting/bye.txt"));
+ }
+
+
+ @Test
+ public void testHttpsSiblingString() throws Exception
+ {
+ // GIVEN
+ String httpUrl = "https://localhost:4242/greeting/hello.txt";
+ Path path = Path.of(new URI(httpUrl));
+
+ // WHEN
+ Path sibling = path.resolveSibling("bye.txt");
+
+ // THEN
+ assertThat(sibling.getFileSystem().getClass().getSimpleName(), equalTo("HttpFileSystem"));
+ assertThat(sibling.getFileSystem().provider().getClass().getSimpleName(), equalTo("HttpsFileSystemProvider"));
+ assertThat(sibling.toString(), equalTo("https://localhost:4242/greeting/bye.txt"));
+ }
+}
diff --git a/runtime/guard-jwt/pom.xml b/runtime/guard-jwt/pom.xml
index 538015ff34..ebba56e5b5 100644
--- a/runtime/guard-jwt/pom.xml
+++ b/runtime/guard-jwt/pom.xml
@@ -68,6 +68,11 @@
${project.version}
test
+
+ ${project.groupId}
+ filesystem-http
+ test
+
junit
junit
diff --git a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardContext.java b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardContext.java
index 55c141be78..3c002ee908 100644
--- a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardContext.java
+++ b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardContext.java
@@ -44,7 +44,7 @@ public JwtGuardHandler attach(
GuardConfig guard)
{
JwtOptionsConfig options = (JwtOptionsConfig) guard.options;
- JwtGuardHandler handler = new JwtGuardHandler(options, context, supplyAuthorizedId, guard.readURL);
+ JwtGuardHandler handler = new JwtGuardHandler(options, context, supplyAuthorizedId);
handlersById.put(guard.id, handler);
return handler;
}
diff --git a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandler.java b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandler.java
index 50ec6342bb..be90da20c5 100644
--- a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandler.java
+++ b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandler.java
@@ -16,6 +16,9 @@
import static org.agrona.LangUtil.rethrowUnchecked;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
@@ -26,7 +29,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.function.LongSupplier;
import jakarta.json.bind.Jsonb;
@@ -65,8 +67,7 @@ public class JwtGuardHandler implements GuardHandler
public JwtGuardHandler(
JwtOptionsConfig options,
EngineContext context,
- LongSupplier supplyAuthorizedId,
- Function readURL)
+ LongSupplier supplyAuthorizedId)
{
this.issuer = options.issuer;
this.audience = options.audience;
@@ -80,8 +81,8 @@ public JwtGuardHandler(
Jsonb jsonb = JsonbBuilder.newBuilder()
.withConfig(config)
.build();
-
- String keysText = readURL.apply(options.keysURL.get());
+ Path keysPath = context.resolvePath(options.keysURL.get());
+ String keysText = readKeys(keysPath);
JwtKeySetConfig jwks = jsonb.fromJson(keysText, JwtKeySetConfig.class);
keysConfig = jwks.keys;
}
@@ -404,4 +405,21 @@ private void unshareIfNecessary()
}
}
}
+
+ private static String readKeys(
+ Path keysPath)
+ {
+ String content = null;
+
+ try
+ {
+ content = Files.readString(keysPath);
+ }
+ catch (IOException ex)
+ {
+ rethrowUnchecked(ex);
+ }
+
+ return content;
+ }
}
diff --git a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapter.java b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapter.java
index d554efe2b4..e6b76f3bee 100644
--- a/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapter.java
+++ b/runtime/guard-jwt/src/main/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapter.java
@@ -14,8 +14,6 @@
*/
package io.aklivity.zilla.runtime.guard.jwt.internal.config;
-import static java.util.stream.Collectors.toList;
-
import java.util.List;
import jakarta.json.Json;
@@ -36,15 +34,20 @@ public final class JwtKeySetConfigAdapter implements JsonbAdapter keysConfig = keysObject
- .getJsonArray(KEYS_NAME)
- .stream()
+ List keysConfig = keysObject.containsKey(KEYS_NAME)
+ ? keysObject.getJsonArray(KEYS_NAME).stream()
.map(JsonValue::asJsonObject)
.map(keyAdapter::adaptFromJson)
- .collect(toList());
+ .toList()
+ : null;
+
return new JwtKeySetConfig(keysConfig);
}
}
diff --git a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandlerTest.java b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandlerTest.java
index 90bd4add79..a19d95da95 100644
--- a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandlerTest.java
+++ b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardHandlerTest.java
@@ -31,7 +31,6 @@
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
-import java.util.function.Function;
import org.agrona.collections.MutableLong;
import org.jose4j.jws.JsonWebSignature;
@@ -46,8 +45,6 @@
public class JwtGuardHandlerTest
{
- private static final Function READ_KEYS_URL = url -> "{}";
-
private EngineContext context;
@Before
@@ -69,7 +66,7 @@ public void shouldAuthorize() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -102,7 +99,7 @@ public void shouldChallengeDuringChallengeWindow() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -131,7 +128,7 @@ public void shouldNotChallengeDuringWindowWithoutSubject() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -159,7 +156,7 @@ public void shouldNotChallengeBeforeChallengeWindow() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -188,7 +185,7 @@ public void shouldNotChallengeAgainDuringChallengeWindow() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -217,7 +214,7 @@ public void shouldNotAuthorizeWhenAlgorithmDiffers() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "test issuer");
@@ -239,7 +236,7 @@ public void shouldNotAuthorizeWhenSignatureInvalid() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "test issuer");
@@ -263,7 +260,7 @@ public void shouldNotAuthorizeWhenIssuerDiffers() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "not test issuer");
@@ -285,7 +282,7 @@ public void shouldNotAuthorizeWhenAudienceDiffers() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "test issuer");
@@ -307,7 +304,7 @@ public void shouldNotAuthorizeWhenExpired() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -332,7 +329,7 @@ public void shouldNotAuthorizeWhenNotYetValid() throws Exception
.audience("testAudience")
.key(RFC7515_RS256_CONFIG)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -359,7 +356,7 @@ public void shouldNotVerifyAuthorizedWhenRolesInsufficient() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
JwtClaims claims = new JwtClaims();
claims.setClaim("iss", "test issuer");
@@ -385,7 +382,7 @@ public void shouldReauthorizeWhenExpirationLater() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -420,7 +417,7 @@ public void shouldReauthorizeWhenScopeBroader() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -456,7 +453,7 @@ public void shouldNotReauthorizeWhenExpirationEarlier() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -491,7 +488,7 @@ public void shouldNotReauthorizeWhenScopeNarrower() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -528,7 +525,7 @@ public void shouldNotReauthorizeWhenSubjectDiffers() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -565,7 +562,7 @@ public void shouldNotReauthorizeWhenContextDiffers() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
@@ -601,7 +598,7 @@ public void shouldDeauthorize() throws Exception
.key(RFC7515_RS256_CONFIG)
.challenge(challenge)
.build();
- JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement, READ_KEYS_URL);
+ JwtGuardHandler guard = new JwtGuardHandler(options, context, new MutableLong(1L)::getAndIncrement);
Instant now = Instant.now();
diff --git a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardIT.java b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardIT.java
index c89f04281c..77457d8578 100644
--- a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardIT.java
+++ b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/JwtGuardIT.java
@@ -23,11 +23,16 @@
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
+import io.aklivity.k3po.runtime.junit.annotation.Specification;
+import io.aklivity.k3po.runtime.junit.rules.K3poRule;
import io.aklivity.zilla.runtime.engine.test.EngineRule;
import io.aklivity.zilla.runtime.engine.test.annotation.Configuration;
public class JwtGuardIT
{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("keys", "io/aklivity/zilla/specs/guard/jwt/config/keys");
+
private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
private final EngineRule engine = new EngineRule()
@@ -37,7 +42,7 @@ public class JwtGuardIT
.clean();
@Rule
- public final TestRule chain = outerRule(engine).around(timeout);
+ public final TestRule chain = outerRule(k3po).around(engine).around(timeout);
@Test
@Configuration("guard.yaml")
@@ -47,13 +52,21 @@ public void shouldInitialize() throws Exception
@Test
@Configuration("guard-keys-dynamic.yaml")
+ @Specification({
+ "${keys}/issuer"
+ })
public void shouldInitializeGuardWithDynamicKeys() throws Exception
{
+ k3po.finish();
}
@Test
@Configuration("guard-keys-implicit.yaml")
+ @Specification({
+ "${keys}/issuer"
+ })
public void shouldInitializeGuardWithImplicitKeys() throws Exception
{
+ k3po.finish();
}
}
diff --git a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapterTest.java b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapterTest.java
index 225813bc1e..1514aff91f 100644
--- a/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapterTest.java
+++ b/runtime/guard-jwt/src/test/java/io/aklivity/zilla/runtime/guard/jwt/internal/config/JwtKeySetConfigAdapterTest.java
@@ -44,6 +44,28 @@ public void initJson()
jsonb = JsonbBuilder.create(config);
}
+ @Test
+ public void shouldReadJwtKeySetWhenKeysMissing()
+ {
+ String text =
+ "{" +
+ "}";
+ JwtKeySetConfig jwksConfig = jsonb.fromJson(text, JwtKeySetConfig.class);
+ assertThat(jwksConfig.keys, nullValue());
+ }
+
+ @Test
+ public void shouldWriteJwtKeySetWithKeysMissing()
+ {
+ JwtKeySetConfig keySetConfig = new JwtKeySetConfig(null);
+ String text = jsonb.toJson(keySetConfig);
+
+ assertThat(text, not(nullValue()));
+ assertThat(text, equalTo(
+ "{" +
+ "}"));
+ }
+
@Test
public void shouldReadJwtKeySet()
{
@@ -81,7 +103,6 @@ public void shouldReadJwtKeySet()
@Test
public void shouldWriteJwtKeySet()
{
-
JwtKeySetConfig keySetConfig = new JwtKeySetConfig(List.of(RFC7515_RS256_CONFIG, RFC7515_ES256_CONFIG));
String text = jsonb.toJson(keySetConfig);
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 7da8fd0543..51ee2464ea 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -51,6 +51,7 @@
exporter-otlp
exporter-prometheus
exporter-stdout
+ filesystem-http
guard-jwt
metrics-grpc
metrics-http
@@ -235,6 +236,11 @@
exporter-stdout
${project.version}
+
+ ${project.groupId}
+ filesystem-http
+ ${project.version}
+
${project.groupId}
guard-jwt
diff --git a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/config/FileSystemOptionsConfig.java b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/config/FileSystemOptionsConfig.java
index e124c1ec23..9a6d4fe725 100644
--- a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/config/FileSystemOptionsConfig.java
+++ b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/config/FileSystemOptionsConfig.java
@@ -15,6 +15,8 @@
*/
package io.aklivity.zilla.runtime.vault.filesystem.config;
+import java.util.LinkedList;
+import java.util.List;
import java.util.function.Function;
import io.aklivity.zilla.runtime.engine.config.OptionsConfig;
@@ -41,8 +43,25 @@ public static FileSystemOptionsConfigBuilder builder(
FileSystemStoreConfig trust,
FileSystemStoreConfig signers)
{
+ super(List.of(), resolveResources(keys, trust));
this.keys = keys;
this.trust = trust;
this.signers = signers;
}
+
+ private static List resolveResources(
+ FileSystemStoreConfig keys,
+ FileSystemStoreConfig trust)
+ {
+ List resources = new LinkedList<>();
+ if (keys != null && keys.store != null)
+ {
+ resources.add(keys.store);
+ }
+ if (trust != null && trust.store != null)
+ {
+ resources.add(trust.store);
+ }
+ return resources;
+ }
}
diff --git a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemContext.java b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemContext.java
index 0b303d4bc6..7b7904d5b9 100644
--- a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemContext.java
+++ b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemContext.java
@@ -15,7 +15,7 @@
*/
package io.aklivity.zilla.runtime.vault.filesystem.internal;
-import java.net.URL;
+import java.nio.file.Path;
import java.util.function.Function;
import io.aklivity.zilla.runtime.engine.Configuration;
@@ -26,7 +26,7 @@
final class FileSystemContext implements VaultContext
{
- private final Function resolvePath;
+ private final Function resolvePath;
FileSystemContext(
Configuration config,
diff --git a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultHandler.java b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultHandler.java
index c465063d99..80fc4d3fbc 100644
--- a/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultHandler.java
+++ b/runtime/vault-filesystem/src/main/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultHandler.java
@@ -16,8 +16,8 @@
package io.aklivity.zilla.runtime.vault.filesystem.internal;
import java.io.InputStream;
-import java.net.URL;
-import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStore.Entry;
import java.security.KeyStore.PrivateKeyEntry;
@@ -50,7 +50,7 @@ public class FileSystemVaultHandler implements VaultHandler
public FileSystemVaultHandler(
FileSystemOptionsConfig options,
- Function resolvePath)
+ Function resolvePath)
{
lookupKey = supplyLookupPrivateKeyEntry(resolvePath, options.keys);
lookupTrust = supplyLookupTrustedCertificateEntry(resolvePath, options.trust);
@@ -94,21 +94,21 @@ public PrivateKeyEntry[] keys(
}
private static Function supplyLookupPrivateKeyEntry(
- Function resolvePath,
+ Function resolvePath,
FileSystemStoreConfig aliases)
{
return supplyLookupAlias(resolvePath, aliases, FileSystemVaultHandler::lookupPrivateKeyEntry);
}
private static Function supplyLookupTrustedCertificateEntry(
- Function resolvePath,
+ Function resolvePath,
FileSystemStoreConfig aliases)
{
return supplyLookupAlias(resolvePath, aliases, FileSystemVaultHandler::lookupTrustedCertificateEntry);
}
private Function, KeyStore.PrivateKeyEntry[]> supplyLookupPrivateKeyEntries(
- Function resolvePath,
+ Function resolvePath,
FileSystemStoreConfig entries)
{
Function, KeyStore.PrivateKeyEntry[]> lookupKeys = p -> null;
@@ -117,9 +117,8 @@ private Function, KeyStore.PrivateKeyEntry[]> supplyLoo
{
try
{
- URL storeURL = resolvePath.apply(entries.store);
- URLConnection connection = storeURL.openConnection();
- try (InputStream input = connection.getInputStream())
+ Path storePath = resolvePath.apply(entries.store);
+ try (InputStream input = Files.newInputStream(storePath))
{
String type = Optional.ofNullable(entries.type).orElse(TYPE_DEFAULT);
char[] password = Optional.ofNullable(entries.password).map(String::toCharArray).orElse(null);
@@ -165,7 +164,7 @@ private Function, KeyStore.PrivateKeyEntry[]> supplyLoo
}
private static Function supplyLookupAlias(
- Function resolvePath,
+ Function resolvePath,
FileSystemStoreConfig aliases,
Lookup lookup)
{
@@ -175,9 +174,8 @@ private static Function supplyLookupAlias(
{
try
{
- URL storeURL = resolvePath.apply(aliases.store);
- URLConnection connection = storeURL.openConnection();
- try (InputStream input = connection.getInputStream())
+ Path storePath = resolvePath.apply(aliases.store);
+ try (InputStream input = Files.newInputStream(storePath))
{
String type = Optional.ofNullable(aliases.type).orElse(TYPE_DEFAULT);
char[] password = Optional.ofNullable(aliases.password).map(String::toCharArray).orElse(null);
diff --git a/runtime/vault-filesystem/src/test/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultTest.java b/runtime/vault-filesystem/src/test/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultTest.java
index fecdd376f6..2f16fdcc88 100644
--- a/runtime/vault-filesystem/src/test/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultTest.java
+++ b/runtime/vault-filesystem/src/test/java/io/aklivity/zilla/runtime/vault/filesystem/internal/FileSystemVaultTest.java
@@ -20,6 +20,9 @@
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Path;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.TrustedCertificateEntry;
@@ -45,7 +48,7 @@ public void shouldResolveServer() throws Exception
.build()
.build();
- FileSystemVaultHandler vault = new FileSystemVaultHandler(options, FileSystemVaultTest.class::getResource);
+ FileSystemVaultHandler vault = new FileSystemVaultHandler(options, FileSystemVaultTest::getResourcePath);
PrivateKeyEntry key = vault.key("localhost");
TrustedCertificateEntry certificate = vault.certificate("clientca");
@@ -70,7 +73,7 @@ public void shouldResolveClient() throws Exception
.build()
.build();
- FileSystemVaultHandler vault = new FileSystemVaultHandler(options, FileSystemVaultTest.class::getResource);
+ FileSystemVaultHandler vault = new FileSystemVaultHandler(options, FileSystemVaultTest::getResourcePath);
PrivateKeyEntry key = vault.key("client1");
PrivateKeyEntry[] signedKeys = vault.keys("clientca");
@@ -80,4 +83,12 @@ public void shouldResolveClient() throws Exception
assertThat(signedKeys.length, equalTo(1));
assertThat(signedKeys[0], not(nullValue()));
}
+
+ public static Path getResourcePath(
+ String resource)
+ {
+ URL url = FileSystemVaultTest.class.getResource(resource);
+ assert url != null;
+ return Path.of(URI.create(url.toString()));
+ }
}
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/client.rpt
index 5093a82e2c..db208d88cd 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/client.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-connect "http://localhost:8080/"
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -23,13 +23,13 @@ write close
read http:status "404" "Not Found"
read closed
-connect "http://localhost:8080/"
+
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
write close
-write notify FIRST_ABORTED
read http:status "200" "OK"
read http:header "Etag" "AAAAAAA"
read '---\n'
@@ -42,6 +42,16 @@ read '---\n'
read closed
+
+connect "http://localhost:8080/zilla.yaml"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write http:header "Prefer" "wait=86400"
+write close
+
+
connect "zilla://streams/app0"
option zilla:window 8192
option zilla:transmission "duplex"
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/server.rpt
index 1e59cdb65c..53f943cb58 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.create.via.http/server.rpt
@@ -14,7 +14,9 @@
# under the License.
#
-accept "http://localhost:8080/"
+accept "http://localhost:8080/zilla.yaml"
+
+
accepted
connected
@@ -25,13 +27,13 @@ write http:status "404" "Not Found"
write http:content-length
write close
+
accepted
connected
read http:method "GET"
read closed
-write await FIRST_ABORTED
write http:status "200" "OK"
write http:content-length
write http:header "Etag" "AAAAAAA"
@@ -45,6 +47,16 @@ write '---\n'
write close
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read http:header "Prefer" "wait=86400"
+read closed
+
+
accept "zilla://streams/app0"
option zilla:window 8192
option zilla:transmission "duplex"
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/client.rpt
index bcf7ee1383..6f658f4624 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/client.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-connect "http://localhost:8080/"
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -31,22 +31,15 @@ read '---\n'
' exit: app0\n'
read closed
-connect "http://localhost:8080/"
+
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write http:header "Prefer" "wait=86400"
write close
-write notify FIRST_CONNECTED
read http:status "404" "Not Found"
read closed
-connect "zilla://streams/app0"
- option zilla:window 8192
- option zilla:transmission "duplex"
-
-connected
-
-write notify CONFIG_DELETED
-write abort
-read abort
\ No newline at end of file
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/server.rpt
index ca7389f572..803a7cba72 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.delete.via.http/server.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-accept "http://localhost:8080/"
+accept "http://localhost:8080/zilla.yaml"
accepted
connected
@@ -33,24 +33,15 @@ write '---\n'
' exit: app0\n'
write close
+
accepted
connected
read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read http:header "Prefer" "wait=86400"
read closed
-write await FIRST_CONNECTED
write http:status "404" "Not Found"
write http:content-length
write close
-
-accept "zilla://streams/app0"
- option zilla:window 8192
- option zilla:transmission "duplex"
-
-accepted
-connected
-
-read await CONFIG_DELETED
-read aborted
-write aborted
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/client.rpt
index 791d9c4a1d..e5a646a6e9 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/client.rpt
@@ -48,8 +48,8 @@ read closed
connect "zilla://streams/app0"
-option zilla:window 8192
-option zilla:transmission "duplex"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
connected
@@ -57,6 +57,6 @@ write abort
read abort
connect "zilla://streams/app1"
-option zilla:window 8192
-option zilla:transmission "duplex"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
connected
\ No newline at end of file
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/server.rpt
index a4130af69d..39d09dd14b 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.no.etag.via.http/server.rpt
@@ -57,7 +57,6 @@ accept "zilla://streams/app0"
accepted
connected
-write notify CONNECTED
read aborted
write aborted
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/client.rpt
index 99779dc0ff..6319e6f586 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/client.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-connect "http://localhost:8080/"
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -31,7 +31,8 @@ read '---\n'
' exit: app0\n'
read closed
-connect "http://localhost:8080/"
+
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -51,16 +52,26 @@ read '---\n'
read closed
+connect "http://localhost:8080/zilla.yaml"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "BBBBBBB"
+write http:header "Prefer" "wait=86400"
+write close
+
+
connect "zilla://streams/app0"
-option zilla:window 8192
-option zilla:transmission "duplex"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
connected
write abort
read abort
+
connect "zilla://streams/app1"
-option zilla:window 8192
-option zilla:transmission "duplex"
-connected
\ No newline at end of file
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/server.rpt
index 1c15cfe74c..6f19b8e998 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.modify.via.http/server.rpt
@@ -33,6 +33,7 @@ write '---\n'
' exit: app0\n'
write close
+
accepted
connected
@@ -55,17 +56,26 @@ write '---\n'
write close
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "BBBBBBB"
+read http:header "Prefer" "wait=86400"
+read closed
+
+
accept "zilla://streams/app0"
option zilla:window 8192
option zilla:transmission "duplex"
accepted
connected
-write notify CONNECTED
read aborted
write aborted
+
accept "zilla://streams/app1"
option zilla:window 8192
option zilla:transmission "duplex"
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/client.rpt
index 26634d7379..c55613190b 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/client.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-connect "http://localhost:8080/"
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -31,7 +31,7 @@ read '---\n'
' exit: app0\n'
read closed
-connect "http://localhost:8080/"
+connect "http://localhost:8080/zilla.yaml"
connected
write http:method "GET"
@@ -44,11 +44,11 @@ read closed
connect "zilla://streams/app0"
-option zilla:window 8192
-option zilla:transmission "duplex"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
connected
connect "zilla://streams/app0"
-option zilla:window 8192
-option zilla:transmission "duplex"
-connected
\ No newline at end of file
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/server.rpt
index 7db63f1708..d51b937052 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/application/reconfigure.server.error.via.http/server.rpt
@@ -14,7 +14,7 @@
# under the License.
#
-accept "http://localhost:8080/"
+accept "http://localhost:8080/zilla.yaml"
accepted
connected
@@ -53,6 +53,6 @@ accept "zilla://streams/app0"
accepted
connected
-write notify CONNECTED
+
accepted
-connected
\ No newline at end of file
+connected
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/client.rpt
index 1aa54a359f..e1c5e4dec0 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/client.rpt
@@ -14,13 +14,6 @@
# under the License.
#
-
-connect "zilla://streams/net0"
- option zilla:transmission "duplex"
- option zilla:window 8192
-connect aborted
-write notify FIRST_ABORTED
-
connect await CONFIG_CREATED
"zilla://streams/net0"
option zilla:transmission "duplex"
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/server.rpt
index b11e21ce12..5867b43b1e 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.create.via.http/server.rpt
@@ -14,13 +14,9 @@
# under the License.
#
-
accept "zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
-rejected
-
-write notify CONFIG_CREATED
accepted
connected
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/client.rpt
index 0cf89384dc..d7c3cbf35f 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/client.rpt
@@ -14,15 +14,6 @@
# under the License.
#
-connect "zilla://streams/net0"
- option zilla:transmission "duplex"
- option zilla:window 8192
-connected
-write notify FIRST_CONNECTED
-
-write aborted
-read abort
-
connect await CONFIG_DELETED
"zilla://streams/net0"
option zilla:transmission "duplex"
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/server.rpt
index b6386becc9..c288e29dbb 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.delete.via.http/server.rpt
@@ -14,15 +14,8 @@
# under the License.
#
-
accept "zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
-accepted
-connected
-
-write notify CONFIG_DELETED
-read abort
-write aborted
rejected
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/client.rpt
index 1c86918d66..b72ea7b4f6 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/client.rpt
@@ -19,6 +19,8 @@ connect "zilla://streams/net0"
option zilla:window 8192
connected
+write notify CONNECTED
+
write aborted
read abort
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/server.rpt
index 0649d05dc8..87be4cb467 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.no.etag.via.http/server.rpt
@@ -17,14 +17,17 @@
accept "zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
+
accepted
connected
write notify CONFIG_CHANGED
read abort
write aborted
+
rejected
+
accept "zilla://streams/net1"
option zilla:transmission "duplex"
option zilla:window 8192
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/client.rpt
index 1c86918d66..b72ea7b4f6 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/client.rpt
@@ -19,6 +19,8 @@ connect "zilla://streams/net0"
option zilla:window 8192
connected
+write notify CONNECTED
+
write aborted
read abort
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/server.rpt
index 0649d05dc8..5804f1545f 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.modify.via.http/server.rpt
@@ -17,14 +17,18 @@
accept "zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
+
accepted
connected
write notify CONFIG_CHANGED
read abort
write aborted
+
+
rejected
+
accept "zilla://streams/net1"
option zilla:transmission "duplex"
option zilla:window 8192
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/client.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/client.rpt
index bd64496704..b79f1b71f2 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/client.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/client.rpt
@@ -19,9 +19,13 @@ connect "zilla://streams/net0"
option zilla:window 8192
connected
+read notify CONNECTED
+
+
connect await SERVER_ERROR
"zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
connected
+
write notify CHECK_RECONFIGURE
diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/server.rpt b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/server.rpt
index 5beb68d59e..0af1c48c59 100644
--- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/server.rpt
+++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/streams/network/reconfigure.server.error.via.http/server.rpt
@@ -17,9 +17,9 @@
accept "zilla://streams/net0"
option zilla:transmission "duplex"
option zilla:window 8192
+
accepted
connected
-write notify SERVER_ERROR
accepted
connected
\ No newline at end of file
diff --git a/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/ApplicationIT.java b/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/ApplicationIT.java
index a165358034..ed9a73beea 100644
--- a/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/ApplicationIT.java
+++ b/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/ApplicationIT.java
@@ -87,7 +87,7 @@ public void shouldNotReconfigureWhenModifiedButParseFailed() throws Exception
@Specification({
"${app}/reconfigure.modify.via.http/client",
"${app}/reconfigure.modify.via.http/server" })
- public void shouldReconfigureWhenModifiedHTTP() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttp() throws Exception
{
k3po.finish();
}
@@ -96,7 +96,7 @@ public void shouldReconfigureWhenModifiedHTTP() throws Exception
@Specification({
"${app}/reconfigure.create.via.http/client",
"${app}/reconfigure.create.via.http/server" })
- public void shouldReconfigureWhenCreatedHTTP() throws Exception
+ public void shouldReconfigureWhenCreatedViaHttp() throws Exception
{
k3po.finish();
}
@@ -105,7 +105,7 @@ public void shouldReconfigureWhenCreatedHTTP() throws Exception
@Specification({
"${app}/reconfigure.delete.via.http/client",
"${app}/reconfigure.delete.via.http/server" })
- public void shouldReconfigureWhenDeletedHTTP() throws Exception
+ public void shouldReconfigureWhenDeletedViaHttp() throws Exception
{
k3po.finish();
}
@@ -115,7 +115,7 @@ public void shouldReconfigureWhenDeletedHTTP() throws Exception
"${app}/reconfigure.modify.no.etag.via.http/server",
"${app}/reconfigure.modify.no.etag.via.http/client"
})
- public void shouldReconfigureWhenModifiedHTTPEtagNotSupported() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttpEtagNotSupported() throws Exception
{
k3po.finish();
}
@@ -125,7 +125,7 @@ public void shouldReconfigureWhenModifiedHTTPEtagNotSupported() throws Exception
"${app}/reconfigure.server.error.via.http/server",
"${app}/reconfigure.server.error.via.http/client"
})
- public void shouldNotReconfigureWhen500Returned() throws Exception
+ public void shouldNotReconfigureViaHttpWhenServerError() throws Exception
{
k3po.finish();
}
diff --git a/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/NetworkIT.java b/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/NetworkIT.java
index c9a29907b2..8330551345 100644
--- a/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/NetworkIT.java
+++ b/specs/engine.spec/src/test/java/io/aklivity/zilla/specs/engine/streams/NetworkIT.java
@@ -87,8 +87,10 @@ public void shouldNotReconfigureWhenModifiedButParseFailed() throws Exception
@Specification({
"${net}/reconfigure.modify.via.http/client",
"${net}/reconfigure.modify.via.http/server" })
- public void shouldReconfigureWhenModifiedHTTP() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttp() throws Exception
{
+ k3po.start();
+ k3po.notifyBarrier("CONFIG_CHANGED");
k3po.finish();
}
@@ -96,8 +98,10 @@ public void shouldReconfigureWhenModifiedHTTP() throws Exception
@Specification({
"${net}/reconfigure.create.via.http/client",
"${net}/reconfigure.create.via.http/server" })
- public void shouldReconfigureWhenCreatedHTTP() throws Exception
+ public void shouldReconfigureWhenCreatedViaHttp() throws Exception
{
+ k3po.start();
+ k3po.notifyBarrier("CONFIG_CREATED");
k3po.finish();
}
@@ -105,8 +109,10 @@ public void shouldReconfigureWhenCreatedHTTP() throws Exception
@Specification({
"${net}/reconfigure.delete.via.http/client",
"${net}/reconfigure.delete.via.http/server" })
- public void shouldReconfigureWhenDeletedHTTP() throws Exception
+ public void shouldReconfigureWhenDeletedViaHttp() throws Exception
{
+ k3po.start();
+ k3po.notifyBarrier("CONFIG_DELETED");
k3po.finish();
}
@@ -115,7 +121,7 @@ public void shouldReconfigureWhenDeletedHTTP() throws Exception
"${net}/reconfigure.modify.no.etag.via.http/server",
"${net}/reconfigure.modify.no.etag.via.http/client"
})
- public void shouldReconfigureWhenModifiedHTTPEtagNotSupported() throws Exception
+ public void shouldReconfigureWhenModifiedViaHttpEtagNotSupported() throws Exception
{
k3po.finish();
}
@@ -125,8 +131,10 @@ public void shouldReconfigureWhenModifiedHTTPEtagNotSupported() throws Exception
"${net}/reconfigure.server.error.via.http/server",
"${net}/reconfigure.server.error.via.http/client"
})
- public void shouldNotReconfigureWhen500Returned() throws Exception
+ public void shouldNotReconfigureViaHttpWhenServerError() throws Exception
{
+ k3po.start();
+ k3po.notifyBarrier("SERVER_ERROR");
k3po.finish();
}
}
diff --git a/specs/filesystem-http.spec/COPYRIGHT b/specs/filesystem-http.spec/COPYRIGHT
new file mode 100644
index 0000000000..0cb10b6f62
--- /dev/null
+++ b/specs/filesystem-http.spec/COPYRIGHT
@@ -0,0 +1,12 @@
+Copyright ${copyrightYears} Aklivity Inc
+
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
diff --git a/specs/filesystem-http.spec/LICENSE b/specs/filesystem-http.spec/LICENSE
new file mode 100644
index 0000000000..f6abb6327b
--- /dev/null
+++ b/specs/filesystem-http.spec/LICENSE
@@ -0,0 +1,114 @@
+ Aklivity Community License Agreement
+ Version 1.0
+
+This Aklivity Community License Agreement Version 1.0 (the “Agreement”) sets
+forth the terms on which Aklivity, Inc. (“Aklivity”) makes available certain
+software made available by Aklivity under this Agreement (the “Software”). BY
+INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE,
+YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO
+SUCH TERMS AND CONDITIONS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING
+THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU
+HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS
+AGREEMENT ON BEHALF OF SUCH ENTITY. “Licensee” means you, an individual, or
+the entity on whose behalf you are receiving the Software.
+
+ 1. LICENSE GRANT AND CONDITIONS.
+
+ 1.1 License. Subject to the terms and conditions of this Agreement,
+ Aklivity hereby grants to Licensee a non-exclusive, royalty-free,
+ worldwide, non-transferable, non-sublicenseable license during the term
+ of this Agreement to: (a) use the Software; (b) prepare modifications and
+ derivative works of the Software; (c) distribute the Software (including
+ without limitation in source code or object code form); and (d) reproduce
+ copies of the Software (the “License”). Licensee is not granted the
+ right to, and Licensee shall not, exercise the License for an Excluded
+ Purpose. For purposes of this Agreement, “Excluded Purpose” means making
+ available any software-as-a-service, platform-as-a-service,
+ infrastructure-as-a-service or other similar online service that competes
+ with Aklivity products or services that provide the Software.
+
+ 1.2 Conditions. In consideration of the License, Licensee’s distribution
+ of the Software is subject to the following conditions:
+
+ (a) Licensee must cause any Software modified by Licensee to carry
+ prominent notices stating that Licensee modified the Software.
+
+ (b) On each Software copy, Licensee shall reproduce and not remove or
+ alter all Aklivity or third party copyright or other proprietary
+ notices contained in the Software, and Licensee must provide the
+ notice below with each copy.
+
+ “This software is made available by Aklivity, Inc., under the
+ terms of the Aklivity Community License Agreement, Version 1.0
+ located at http://www.Aklivity.io/Aklivity-community-license. BY
+ INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF
+ THE SOFTWARE, YOU AGREE TO THE TERMS OF SUCH LICENSE AGREEMENT.”
+
+ 1.3 Licensee Modifications. Licensee may add its own copyright notices
+ to modifications made by Licensee and may provide additional or different
+ license terms and conditions for use, reproduction, or distribution of
+ Licensee’s modifications. While redistributing the Software or
+ modifications thereof, Licensee may choose to offer, for a fee or free of
+ charge, support, warranty, indemnity, or other obligations. Licensee, and
+ not Aklivity, will be responsible for any such obligations.
+
+ 1.4 No Sublicensing. The License does not include the right to
+ sublicense the Software, however, each recipient to which Licensee
+ provides the Software may exercise the Licenses so long as such recipient
+ agrees to the terms and conditions of this Agreement.
+
+ 2. TERM AND TERMINATION. This Agreement will continue unless and until
+ earlier terminated as set forth herein. If Licensee breaches any of its
+ conditions or obligations under this Agreement, this Agreement will
+ terminate automatically and the License will terminate automatically and
+ permanently.
+
+ 3. INTELLECTUAL PROPERTY. As between the parties, Aklivity will retain all
+ right, title, and interest in the Software, and all intellectual property
+ rights therein. Aklivity hereby reserves all rights not expressly granted
+ to Licensee in this Agreement. Aklivity hereby reserves all rights in its
+ trademarks and service marks, and no licenses therein are granted in this
+ Agreement.
+
+ 4. DISCLAIMER. Aklivity HEREBY DISCLAIMS ANY AND ALL WARRANTIES AND
+ CONDITIONS, EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, AND SPECIFICALLY
+ DISCLAIMS ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR
+ PURPOSE, WITH RESPECT TO THE SOFTWARE.
+
+ 5. LIMITATION OF LIABILITY. Aklivity WILL NOT BE LIABLE FOR ANY DAMAGES OF
+ ANY KIND, INCLUDING BUT NOT LIMITED TO, LOST PROFITS OR ANY CONSEQUENTIAL,
+ SPECIAL, INCIDENTAL, INDIRECT, OR DIRECT DAMAGES, HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, ARISING OUT OF THIS AGREEMENT. THE FOREGOING SHALL
+ APPLY TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+
+ 6.GENERAL.
+
+ 6.1 Governing Law. This Agreement will be governed by and interpreted in
+ accordance with the laws of the state of California, without reference to
+ its conflict of laws principles. If Licensee is located within the
+ United States, all disputes arising out of this Agreement are subject to
+ the exclusive jurisdiction of courts located in Santa Clara County,
+ California. USA. If Licensee is located outside of the United States,
+ any dispute, controversy or claim arising out of or relating to this
+ Agreement will be referred to and finally determined by arbitration in
+ accordance with the JAMS International Arbitration Rules. The tribunal
+ will consist of one arbitrator. The place of arbitration will be Palo
+ Alto, California. The language to be used in the arbitral proceedings
+ will be English. Judgment upon the award rendered by the arbitrator may
+ be entered in any court having jurisdiction thereof.
+
+ 6.2 Assignment. Licensee is not authorized to assign its rights under
+ this Agreement to any third party. Aklivity may freely assign its rights
+ under this Agreement to any third party.
+
+ 6.3 Other. This Agreement is the entire agreement between the parties
+ regarding the subject matter hereof. No amendment or modification of
+ this Agreement will be valid or binding upon the parties unless made in
+ writing and signed by the duly authorized representatives of both
+ parties. In the event that any provision, including without limitation
+ any condition, of this Agreement is held to be unenforceable, this
+ Agreement and all licenses and rights granted hereunder will immediately
+ terminate. Waiver by Aklivity of a breach of any provision of this
+ Agreement or the failure by Aklivity to exercise any right hereunder
+ will not be construed as a waiver of any subsequent breach of that right
+ or as a waiver of any other right.
\ No newline at end of file
diff --git a/specs/filesystem-http.spec/NOTICE b/specs/filesystem-http.spec/NOTICE
new file mode 100644
index 0000000000..9024d8926d
--- /dev/null
+++ b/specs/filesystem-http.spec/NOTICE
@@ -0,0 +1,13 @@
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+This project includes:
+
diff --git a/specs/filesystem-http.spec/NOTICE.template b/specs/filesystem-http.spec/NOTICE.template
new file mode 100644
index 0000000000..209ca12f74
--- /dev/null
+++ b/specs/filesystem-http.spec/NOTICE.template
@@ -0,0 +1,13 @@
+Licensed under the Aklivity Community License (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.aklivity.io/aklivity-community-license/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+This project includes:
+#GENERATED_NOTICES#
diff --git a/specs/filesystem-http.spec/mvnw b/specs/filesystem-http.spec/mvnw
new file mode 100755
index 0000000000..d2f0ea3808
--- /dev/null
+++ b/specs/filesystem-http.spec/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/specs/filesystem-http.spec/mvnw.cmd b/specs/filesystem-http.spec/mvnw.cmd
new file mode 100644
index 0000000000..b26ab24f03
--- /dev/null
+++ b/specs/filesystem-http.spec/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/specs/filesystem-http.spec/pom.xml b/specs/filesystem-http.spec/pom.xml
new file mode 100644
index 0000000000..98bf6f77aa
--- /dev/null
+++ b/specs/filesystem-http.spec/pom.xml
@@ -0,0 +1,123 @@
+
+
+
+ 4.0.0
+
+ io.aklivity.zilla
+ specs
+ develop-SNAPSHOT
+ ../pom.xml
+
+
+ filesystem-http.spec
+ zilla::specs::filesystem-http.spec
+
+
+
+ Aklivity Community License Agreement
+ https://www.aklivity.io/aklivity-community-license/
+ repo
+
+
+
+
+ 1.00
+ 0
+
+
+
+
+ junit
+ junit
+ test
+
+
+ io.aklivity.k3po
+ lang
+ provided
+
+
+ io.aklivity.k3po
+ control-junit
+ test
+
+
+ org.hamcrest
+ hamcrest-library
+ test
+
+
+
+
+
+
+ src/main/resources
+
+
+ src/main/scripts
+
+
+
+
+
+ org.jasig.maven
+ maven-notice-plugin
+
+
+ com.mycila
+ license-maven-plugin
+
+
+ maven-checkstyle-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.moditect
+ moditect-maven-plugin
+
+
+ io.aklivity.k3po
+ k3po-maven-plugin
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ ${jacoco.coverage.ratio}
+
+
+ CLASS
+ MISSEDCOUNT
+ ${jacoco.missed.count}
+
+
+
+
+
+
+
+
+
diff --git a/specs/filesystem-http.spec/src/main/moditect/module-info.java b/specs/filesystem-http.spec/src/main/moditect/module-info.java
new file mode 100644
index 0000000000..a7fac52d06
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/moditect/module-info.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+open module io.aklivity.zilla.specs.filesystem.http
+{
+}
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/client.rpt
new file mode 100644
index 0000000000..7709573bb1
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/client.rpt
@@ -0,0 +1,36 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "404" "Not Found"
+read notify FIRST_READ
+read closed
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write await FIRST_READ
+write http:method "GET"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/server.rpt
new file mode 100644
index 0000000000..74dfcfc1a7
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound.success/server.rpt
@@ -0,0 +1,37 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "404" "Not Found"
+write http:content-length
+write close
+
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/client.rpt
new file mode 100644
index 0000000000..c373f679d7
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/client.rpt
@@ -0,0 +1,23 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/notfound.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "404" "Not Found"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/server.rpt
new file mode 100644
index 0000000000..179a436744
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.notfound/server.rpt
@@ -0,0 +1,25 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/notfound.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "404" "Not Found"
+write http:content-length
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/client.rpt
new file mode 100644
index 0000000000..27e498af0c
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/client.rpt
@@ -0,0 +1,39 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read notify FIRST_READ
+read closed
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write await FIRST_READ
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "BBBBBBB"
+read "Hello Universe!"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/server.rpt
new file mode 100644
index 0000000000..0052eb0bf2
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.modified/server.rpt
@@ -0,0 +1,40 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "BBBBBBB"
+write "Hello Universe!"
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/client.rpt
new file mode 100644
index 0000000000..68a283c0c5
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/client.rpt
@@ -0,0 +1,38 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read notify FIRST_READ
+read closed
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write await FIRST_READ
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write close
+
+read http:status "304" "Not Modified"
+read http:header "Etag" "AAAAAAA"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/server.rpt
new file mode 100644
index 0000000000..1c7c544b51
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success.etag.not.modified/server.rpt
@@ -0,0 +1,39 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read closed
+
+write http:status "304" "Not Modified"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/client.rpt
new file mode 100644
index 0000000000..c6134a366a
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/client.rpt
@@ -0,0 +1,25 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/server.rpt
new file mode 100644
index 0000000000..fc0194bdbd
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/read.success/server.rpt
@@ -0,0 +1,27 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/client.rpt
new file mode 100644
index 0000000000..44ef7dc8fe
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/client.rpt
@@ -0,0 +1,78 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read closed
+read notify CONFIG_INITIALIZED
+
+
+connect await CONFIG_INITIALIZED
+ "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write http:header "Prefer" "wait=86400"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read closed
+read notify CONFIG_IDENTICAL
+
+connect await CONFIG_IDENTICAL
+ "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write http:header "Prefer" "wait=86400"
+write close
+
+read http:status "200" "OK"
+read http:header "Etag" "BBBBBBB"
+read "Hello Universe!"
+read closed
+read notify CONFIG_MODIFIED
+
+connect await CONFIG_MODIFIED
+ "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "BBBBBBB"
+write http:header "Prefer" "wait=86400"
+write close
+
+read http:status "304" "Not changed"
+read closed
+read notify CONFIG_NOT_MODIFIED
+
+connect await CONFIG_NOT_MODIFIED
+ "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "BBBBBBB"
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/server.rpt
new file mode 100644
index 0000000000..c1d51dd9b1
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch.read/server.rpt
@@ -0,0 +1,83 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
+
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read http:header "Prefer" "wait=86400"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
+
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read http:header "Prefer" "wait=86400"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "BBBBBBB"
+write "Hello Universe!"
+write close
+
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "BBBBBBB"
+read http:header "Prefer" "wait=86400"
+read closed
+
+write http:status "304" "Not changed"
+write http:content-length
+write close
+
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "BBBBBBB"
+read closed
+
+write http:status "304" "Not changed"
+write http:content-length
+write close
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/client.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/client.rpt
new file mode 100644
index 0000000000..28bac19a0b
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/client.rpt
@@ -0,0 +1,40 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write close
+
+read notify REGISTERED
+read http:status "200" "OK"
+read http:header "Etag" "AAAAAAA"
+read "Hello World!"
+read closed
+
+connect "http://localhost:8080/hello.txt"
+connected
+
+write http:method "GET"
+write http:header "If-None-Match" "AAAAAAA"
+write http:header "Prefer" "wait=86400"
+write close
+
+read notify MODIFIED
+read http:status "200" "OK"
+read http:header "Etag" "BBBBBBB"
+read "Hello Universe!"
+read closed
diff --git a/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/server.rpt b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/server.rpt
new file mode 100644
index 0000000000..5b330c1ffc
--- /dev/null
+++ b/specs/filesystem-http.spec/src/main/scripts/io/aklivity/zilla/specs/filesystem/http/application/watch/server.rpt
@@ -0,0 +1,43 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/hello.txt"
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write await REGISTERED
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "AAAAAAA"
+write "Hello World!"
+write close
+
+accepted
+connected
+
+read http:method "GET"
+read http:header "If-None-Match" "AAAAAAA"
+read http:header "Prefer" "wait=86400"
+read closed
+
+write await MODIFIED
+write http:status "200" "OK"
+write http:content-length
+write http:header "Etag" "BBBBBBB"
+write "Hello Universe!"
+write close
diff --git a/specs/filesystem-http.spec/src/test/java/io/aklivity/zilla/specs/filesystem/http/ApplicationIT.java b/specs/filesystem-http.spec/src/test/java/io/aklivity/zilla/specs/filesystem/http/ApplicationIT.java
new file mode 100644
index 0000000000..54003f25fe
--- /dev/null
+++ b/specs/filesystem-http.spec/src/test/java/io/aklivity/zilla/specs/filesystem/http/ApplicationIT.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc
+ *
+ * Licensed under the Aklivity Community License (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain a copy of the
+ * License at
+ *
+ * https://www.aklivity.io/aklivity-community-license/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.aklivity.zilla.specs.filesystem.http;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import io.aklivity.k3po.runtime.junit.annotation.Specification;
+import io.aklivity.k3po.runtime.junit.rules.K3poRule;
+
+public class ApplicationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("app", "io/aklivity/zilla/specs/filesystem/http/application");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(5, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${app}/read.success/client",
+ "${app}/read.success/server" })
+ public void shouldReadString() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.success.etag.not.modified/client",
+ "${app}/read.success.etag.not.modified/server" })
+ public void shouldReadStringEtagNotModified() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.success.etag.modified/client",
+ "${app}/read.success.etag.modified/server" })
+ public void shouldReadStringEtagModified() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.notfound/client",
+ "${app}/read.notfound/server" })
+ public void shouldReadStringNotFound() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/read.notfound.success/client",
+ "${app}/read.notfound.success/server" })
+ public void shouldReadStringNotFoundSuccess() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/watch/client",
+ "${app}/watch/server" })
+ public void shouldWatch() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/watch.read/client",
+ "${app}/watch.read/server" })
+ public void shouldWatchRead() throws Exception
+ {
+ k3po.finish();
+ }
+}
diff --git a/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-dynamic.yaml b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-dynamic.yaml
index f877141e5b..d4663cd0f9 100644
--- a/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-dynamic.yaml
+++ b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-dynamic.yaml
@@ -13,20 +13,13 @@
# specific language governing permissions and limitations under the License.
#
-{
- "name": "test",
- "guards":
- {
- "jwt0":
- {
- "type": "jwt",
- "options":
- {
- "issuer": "https://aklivity.us.auth0.com",
- "audience": "https://api.aklivity.com",
- "keys": "https://aklivity.us.auth0.com/.well-known/jwks.json",
- "challenge": 30
- }
- }
- }
-}
+---
+name: test
+guards:
+ jwt0:
+ type: jwt
+ options:
+ issuer: http://localhost:8080
+ audience: https://api.aklivity.com
+ keys: http://localhost:8080/.well-known/jwks.json
+ challenge: 30
diff --git a/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-implicit.yaml b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-implicit.yaml
index 8994c3c306..138928094f 100644
--- a/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-implicit.yaml
+++ b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/guard-keys-implicit.yaml
@@ -19,6 +19,6 @@ guards:
jwt0:
type: jwt
options:
- issuer: https://aklivity.us.auth0.com
+ issuer: http://localhost:8080
audience: https://api.aklivity.com
challenge: 30
diff --git a/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/keys/issuer.rpt b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/keys/issuer.rpt
new file mode 100644
index 0000000000..7be87bde1e
--- /dev/null
+++ b/specs/guard-jwt.spec/src/main/scripts/io/aklivity/zilla/specs/guard/jwt/config/keys/issuer.rpt
@@ -0,0 +1,29 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+accept "http://localhost:8080/.well-known/jwks.json"
+
+
+accepted
+connected
+
+read http:method "GET"
+read closed
+
+write http:status "200" "OK"
+write http:content-length
+write '{}'
+
+write close
diff --git a/specs/pom.xml b/specs/pom.xml
index 29bb1d9f32..1dfc440066 100644
--- a/specs/pom.xml
+++ b/specs/pom.xml
@@ -46,6 +46,7 @@
exporter-otlp.spec
exporter-prometheus.spec
exporter-stdout.spec
+ filesystem-http.spec
metrics-stream.spec
metrics-http.spec
metrics-grpc.spec