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 options, + FileAttribute... attrs) + { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public AsynchronousFileChannel newAsynchronousFileChannel( + Path path, + Set options, + ExecutorService executor, + FileAttribute... attrs) + { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public SeekableByteChannel newByteChannel( + Path path, + Set options, + FileAttribute... attrs) + { + HttpPath httpPath = checkPath(path); + return new ReadOnlyByteArrayChannel(httpPath.readBody()); + } + + @Override + public DirectoryStream newDirectoryStream( + Path dir, + DirectoryStream.Filter 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