diff --git a/README-CHANGES.xml b/README-CHANGES.xml index c86a0c9..859ca77 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -11,12 +11,13 @@ - + - + + diff --git a/README-LICENSE.txt b/README-LICENSE.txt index f627468..8872798 100644 --- a/README-LICENSE.txt +++ b/README-LICENSE.txt @@ -1,4 +1,4 @@ -Copyright © 2023 Mark Raynsford https://www.io7m.com +Copyright © 2024 Mark Raynsford https://www.io7m.com Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.in b/README.in index 01ed361..b67417c 100644 --- a/README.in +++ b/README.in @@ -9,7 +9,7 @@ A tiny embedded HTTP server for unit testing. * Conveniently enqueue responses to arbitrary requests. * Verify that requests were received as expected. * Zero dependencies. - * Written in pure Java 17. + * Written in pure Java 21. * [OSGi](https://www.osgi.org/) ready. * [JPMS](https://en.wikipedia.org/wiki/Java_Platform_Module_System) ready. * ISC license. @@ -82,3 +82,155 @@ public void tearDown() this.server.close(); } ``` + +### OCI + +The `quixote` server is also capable of acting as a standalone web server +for use in integration tests involving containers. This is primarily useful +for situations where an integration test starts multiple containers inside +an isolated virtual network, and one or more of the test containers need to +speak to a fake web server, and the host running the test suite needs to +observe the requests in question. This particular configuration can be +difficult to set up in an automated manner, as most virtual networking +solutions don't allow for containers to "talk back" to the host machine. Using +`quixote` as a container allows for it to be present inside the virtual network, +and for the host to download and parse request logs after the test has +completed, sidestepping the need for any containers to know about the host +machine. + +In other words: + +1. The host creates a virtual network using its container engine of choice. +2. The host creates several containers inside the virtual network. +3. The host writes a `quixote` configuration file in the host directory. +4. The host creates a `quixote` container inside the virtual network, mounting + the host directory into the container as a volume mount. +5. The host runs the test. This causes the containers inside the virtual + network to make requests to the `quixote` container. +6. The host reads the resulting request log from the host directory and + verifies that the correct requests were made for the test. + +![Virtual Network](net.png) + +Given a configuration file conforming to the provided +[schema](com.io7m.quixote.xml/src/main/resources/com/io7m/quixote/xml/configuration-1.xsd), +the `quixote` server can be started using a container engine such as +[Podman](https://podman.io). + +``` +$ cat quixote/config.xml + + + + + + + +
+ + Hello world! + + + +``` + +``` +$ podman run \ + --rm \ + --interactive \ + --tty \ + --volume quixote:/quixote/data:rw \ + --publish 20001:20001/tcp \ + quay.io/io7mcom/quixote:1.2.0-SNAPSHOT \ + /quixote/data/config.xml \ + /quixote/data/output.bin +INFO com.io7m.quixote.main.Main: Quixote running at http://localhost:20001/ +``` + +``` +$ curl http://localhost:20001/ +Hello world! +``` + +The server appends every received request to a file consisting of an +array of request records in a trivial binary format. A request record has the +following structure: + +``` +RequestRecord +{ + Unsigned64 quixote; + Unsigned32 version; + Unsigned64 length; + Unsigned8 data[length]; +} +``` + +All integer values are in big-endian byte order. + +The `quixote` field of the `RequestRecord` structure always has the value +`0x515549584F544521` ("QUIXOTE!" in ASCII). The `version` field of the +`RequestRecord` field currently has the value `0x00000001`. + +The `data` field is a sequence of `length` bytes that represents a serialized +[java.util.Properties](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Properties.html) +XML document. For example, the `curl` request above resulted in the following +record being appended to `/quixote/output.bin`: + +``` +0000:0000 | 51 55 49 58 4F 54 45 21 00 00 00 01 00 00 00 00 | QUIXOTE!........ +0000:0010 | 00 00 01 BF 3C 3F 78 6D 6C 20 76 65 72 73 69 6F | ...¿. +0000:0080 | 0A 3C 70 72 6F 70 65 72 74 69 65 73 3E 0A 3C 65 | ..12 +0000:00B0 | 37 2E 30 2E 30 2E 31 3C 2F 65 6E 74 72 79 3E 0A | 7.0.0.1. +0000:00C0 | 3C 65 6E 74 72 79 20 6B 65 79 3D 22 49 6E 66 6F | GET.127.0.0. +0000:0110 | 31 3C 2F 65 6E 74 72 79 3E 0A 3C 65 6E 74 72 79 | 1./.*/*.curl/8.7.1.127 +0000:01B0 | 2E 30 2E 30 2E 31 3A 32 30 30 30 31 3C 2F 65 6E | .0.0.1:20001.. +``` + +Extracting the XML document and formatting it gives: + +``` + + + +127.0.0.1 +GET +127.0.0.1 +/ +*/* +curl/8.7.1 +127.0.0.1:20001 + +``` + +We can see that the client made a `GET` request to the `/` path. The +[QWebRequestLogging](com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebRequestLogging.java) +class provides convenient functions to read and write request logs. diff --git a/README.md b/README.md index b9c1cff..93227a1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A tiny embedded HTTP server for unit testing. * Conveniently enqueue responses to arbitrary requests. * Verify that requests were received as expected. * Zero dependencies. - * Written in pure Java 17. + * Written in pure Java 21. * [OSGi](https://www.osgi.org/) ready. * [JPMS](https://en.wikipedia.org/wiki/Java_Platform_Module_System) ready. * ISC license. @@ -98,3 +98,155 @@ public void tearDown() } ``` +### OCI + +The `quixote` server is also capable of acting as a standalone web server +for use in integration tests involving containers. This is primarily useful +for situations where an integration test starts multiple containers inside +an isolated virtual network, and one or more of the test containers need to +speak to a fake web server, and the host running the test suite needs to +observe the requests in question. This particular configuration can be +difficult to set up in an automated manner, as most virtual networking +solutions don't allow for containers to "talk back" to the host machine. Using +`quixote` as a container allows for it to be present inside the virtual network, +and for the host to download and parse request logs after the test has +completed, sidestepping the need for any containers to know about the host +machine. + +In other words: + +1. The host creates a virtual network using its container engine of choice. +2. The host creates several containers inside the virtual network. +3. The host writes a `quixote` configuration file in the host directory. +4. The host creates a `quixote` container inside the virtual network, mounting + the host directory into the container as a volume mount. +5. The host runs the test. This causes the containers inside the virtual + network to make requests to the `quixote` container. +6. The host reads the resulting request log from the host directory and + verifies that the correct requests were made for the test. + +![Virtual Network](net.png) + +Given a configuration file conforming to the provided +[schema](com.io7m.quixote.xml/src/main/resources/com/io7m/quixote/xml/configuration-1.xsd), +the `quixote` server can be started using a container engine such as +[Podman](https://podman.io). + +``` +$ cat quixote/config.xml + + + + + + + +
+ + Hello world! + + + +``` + +``` +$ podman run \ + --rm \ + --interactive \ + --tty \ + --volume quixote:/quixote/data:rw \ + --publish 20001:20001/tcp \ + quay.io/io7mcom/quixote:1.2.0-SNAPSHOT \ + /quixote/data/config.xml \ + /quixote/data/output.bin +INFO com.io7m.quixote.main.Main: Quixote running at http://localhost:20001/ +``` + +``` +$ curl http://localhost:20001/ +Hello world! +``` + +The server appends every received request to a file consisting of an +array of request records in a trivial binary format. A request record has the +following structure: + +``` +RequestRecord +{ + Unsigned64 quixote; + Unsigned32 version; + Unsigned64 length; + Unsigned8 data[length]; +} +``` + +All integer values are in big-endian byte order. + +The `quixote` field of the `RequestRecord` structure always has the value +`0x515549584F544521` ("QUIXOTE!" in ASCII). The `version` field of the +`RequestRecord` field currently has the value `0x00000001`. + +The `data` field is a sequence of `length` bytes that represents a serialized +[java.util.Properties](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Properties.html) +XML document. For example, the `curl` request above resulted in the following +record being appended to `/quixote/output.bin`: + +``` +0000:0000 | 51 55 49 58 4F 54 45 21 00 00 00 01 00 00 00 00 | QUIXOTE!........ +0000:0010 | 00 00 01 BF 3C 3F 78 6D 6C 20 76 65 72 73 69 6F | ...¿. +0000:0080 | 0A 3C 70 72 6F 70 65 72 74 69 65 73 3E 0A 3C 65 | ..12 +0000:00B0 | 37 2E 30 2E 30 2E 31 3C 2F 65 6E 74 72 79 3E 0A | 7.0.0.1. +0000:00C0 | 3C 65 6E 74 72 79 20 6B 65 79 3D 22 49 6E 66 6F | GET.127.0.0. +0000:0110 | 31 3C 2F 65 6E 74 72 79 3E 0A 3C 65 6E 74 72 79 | 1./.*/*.curl/8.7.1.127 +0000:01B0 | 2E 30 2E 30 2E 31 3A 32 30 30 30 31 3C 2F 65 6E | .0.0.1:20001.. +``` + +Extracting the XML document and formatting it gives: + +``` + + + +127.0.0.1 +GET +127.0.0.1 +/ +*/* +curl/8.7.1 +127.0.0.1:20001 + +``` + +We can see that the client made a `GET` request to the `/` path. The +[QWebRequestLogging](com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebRequestLogging.java) +class provides convenient functions to read and write request logs. + diff --git a/checkstyle-filter.xml b/checkstyle-filter.xml index 7f4a289..3f81f3a 100644 --- a/checkstyle-filter.xml +++ b/checkstyle-filter.xml @@ -5,5 +5,8 @@ "https://checkstyle.org/dtds/suppressions_1_0.dtd"> - + + diff --git a/com.io7m.quixote.core/src/main/java-descriptor-testing/module-info.java b/com.io7m.quixote.core/src/main/java-descriptor-testing/module-info.java new file mode 100644 index 0000000..46d48f1 --- /dev/null +++ b/com.io7m.quixote.core/src/main/java-descriptor-testing/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2022 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (Core) + */ + +module com.io7m.quixote.core +{ + requires static org.osgi.annotation.bundle; + requires static org.osgi.annotation.versioning; + + requires java.logging; + requires nanohttpd; + + exports com.io7m.quixote.core; +} diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebConfiguration.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebConfiguration.java new file mode 100644 index 0000000..026f87f --- /dev/null +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.core; + +import java.util.List; +import java.util.Objects; + +/** + * Configuration for a server. + * + * @param serverConfiguration The server configuration + * @param responses The canned responses + */ + +public record QWebConfiguration( + QWebServerConfiguration serverConfiguration, + List responses) +{ + /** + * Configuration for a server. + * + * @param serverConfiguration The server configuration + * @param responses The canned responses + */ + + public QWebConfiguration + { + Objects.requireNonNull(serverConfiguration, "serverConfiguration"); + responses = List.copyOf(responses); + } +} diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebRequestLogging.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebRequestLogging.java new file mode 100644 index 0000000..e7492df --- /dev/null +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebRequestLogging.java @@ -0,0 +1,212 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Functions to read and write logs in the binary request log format. + */ + +public final class QWebRequestLogging +{ + private static final byte[] HEADER; + + static { + try (var out = new ByteArrayOutputStream()) { + out.writeBytes("QUIXOTE!".getBytes(UTF_8)); + out.write('\0'); + out.write('\0'); + out.write('\0'); + out.write('\1'); + out.flush(); + HEADER = out.toByteArray(); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + private QWebRequestLogging() + { + + } + + /** + * Append a request to the given log. + * + * @param outputLog The output stream + * @param request The request + * + * @throws IOException On I/O errors + */ + + public static void append( + final OutputStream outputLog, + final QWebRequestReceivedType request) + throws IOException + { + serializeRequest(outputLog, request); + } + + private static void serializeRequest( + final OutputStream outputLog, + final QWebRequestReceivedType request) + throws IOException + { + outputLog.write(HEADER); + outputLog.flush(); + + final var properties = new Properties(); + properties.setProperty("Info.Path", request.path()); + properties.setProperty("Info.Method", request.method()); + + for (final var entry : request.headers().entrySet()) { + properties.setProperty( + "Header.%s".formatted(entry.getKey()), + entry.getValue() + ); + } + + for (final var entry : request.files().entrySet()) { + properties.setProperty( + "File.%s".formatted(entry.getKey()), + entry.getValue() + ); + } + + try (var textStream = new ByteArrayOutputStream()) { + properties.storeToXML(textStream, "", UTF_8); + textStream.flush(); + writeBytes(outputLog, textStream.toByteArray()); + } + + outputLog.flush(); + } + + private static void writeBytes( + final OutputStream outputLog, + final byte[] data) + throws IOException + { + final var lengthArray = + new byte[8]; + final var length = + ByteBuffer.wrap(lengthArray) + .order(ByteOrder.BIG_ENDIAN); + + length.putLong(0, Integer.toUnsignedLong(data.length)); + outputLog.write(lengthArray); + outputLog.write(data); + } + + /** + * Read a request from the given log. + * + * @param inputLog The input stream + * + * @return The parsed request properties + * + * @throws IOException On I/O errors + */ + + public static QWebRequestReceivedType read( + final InputStream inputLog) + throws IOException + { + final var header = + inputLog.readNBytes(12); + + if (!Arrays.equals(header, HEADER)) { + throw new IOException( + "Invalid header: %s".formatted(HexFormat.of().formatHex(header)) + ); + } + + final var lengthArray = + inputLog.readNBytes(8); + + final var lengthBuffer = + ByteBuffer.wrap(lengthArray) + .order(ByteOrder.BIG_ENDIAN); + + final var length = + lengthBuffer.getLong(0); + + final var body = + inputLog.readNBytes(Math.toIntExact(length)); + + try (var bodyStream = new ByteArrayInputStream(body)) { + final var properties = new Properties(); + properties.loadFromXML(bodyStream); + return new ImmutableRequest(properties); + } + } + + private record ImmutableRequest( + Properties properties) + implements QWebRequestReceivedType + { + + @Override + public String method() + { + return this.properties.getProperty("Info.Method"); + } + + @Override + public String path() + { + return this.properties.getProperty("Info.Path"); + + } + + @Override + public Map headers() + { + return this.properties.stringPropertyNames() + .stream() + .filter(x -> x.startsWith("Header.")) + .map(x -> Map.entry(x, this.properties.getProperty(x))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public Map files() + { + return this.properties.stringPropertyNames() + .stream() + .filter(x -> x.startsWith("File.")) + .map(x -> Map.entry(x, this.properties.getProperty(x))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } +} diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebResponseRecorded.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebResponseRecorded.java new file mode 100644 index 0000000..71a5e22 --- /dev/null +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebResponseRecorded.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package com.io7m.quixote.core; + +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A recorded response. + * + * @param method The method for which this response will be returned + * @param path The path for which this response will be returned + * @param statusCode The status code + * @param headers The headers + * @param content The content + */ + +public record QWebResponseRecorded( + Pattern method, + Pattern path, + int statusCode, + Map headers, + byte[] content) +{ + /** + * A recorded response. + * + * @param method The method for which this response will be returned + * @param path The path for which this response will be returned + * @param statusCode The status code + * @param headers The headers + * @param content The content + */ + + public QWebResponseRecorded + { + Objects.requireNonNull(method, "method"); + Objects.requireNonNull(path, "path"); + headers = Map.copyOf(headers); + content = content.clone(); + } +} diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerConfiguration.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerConfiguration.java new file mode 100644 index 0000000..cd2f193 --- /dev/null +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.core; + +import java.util.Objects; + +/** + * The web server configuration. + * + * @param hostName The hostname to which to bind the server + * @param port The port to which to bind the server + * @param enableGZIP Enable/disable GZIP + */ + +public record QWebServerConfiguration( + String hostName, + int port, + boolean enableGZIP) +{ + /** + * The web server configuration. + * + * @param hostName The hostname to which to bind the server + * @param port The port to which to bind the server + * @param enableGZIP Enable/disable GZIP + */ + + public QWebServerConfiguration + { + Objects.requireNonNull(hostName, "hostName"); + } +} diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerFactoryType.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerFactoryType.java index 4e0b464..84a3cef 100644 --- a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerFactoryType.java +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerFactoryType.java @@ -62,7 +62,7 @@ QWebServerType createForAll(int port) * address. * * @param address The address - * @param port The port + * @param port The port * * @return A new web server * @@ -74,4 +74,19 @@ QWebServerType createForSpecific( InetAddress address, int port) throws IOException; + + /** + * Create a new web server from the given configuration. + * + * @param configuration The configuration + * + * @return A new web server + * + * @throws IOException On errors + * @since 1.2.0 + */ + + QWebServerType createForConfiguration( + QWebConfiguration configuration) + throws IOException; } diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerType.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerType.java index 20641b4..dc1e173 100644 --- a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerType.java +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServerType.java @@ -21,6 +21,7 @@ import java.io.Closeable; import java.net.URI; import java.util.List; +import java.util.function.Consumer; /** * A web server. @@ -67,4 +68,13 @@ QWebServerType enableGzip( */ List requestsReceived(); + + /** + * Set the callback that will be evaluated on each request. + * + * @param onRequest The request receiver + */ + + void setRequestCallback( + Consumer onRequest); } diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServers.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServers.java index b9a1b68..07a569d 100644 --- a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServers.java +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/QWebServers.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; import java.util.regex.Pattern; import static fi.iki.elonen.NanoHTTPD.Response.Status.SERVICE_UNAVAILABLE; @@ -103,6 +104,22 @@ public static QWebServerType createServerForSpecific( return new QWebServers().createForSpecific(address, port); } + /** + * Create a new web server from the given configuration. + * + * @param configuration The configuration + * + * @return A new web server + * + * @throws IOException On errors + */ + + public static QWebServerType createServerForConfiguration( + final QWebConfiguration configuration) + throws IOException + { + return new QWebServers().createForConfiguration(configuration); + } @Override public QWebServerType create( @@ -129,6 +146,36 @@ public QWebServerType createForSpecific( return new QWebServer(address.getHostName(), port); } + @Override + public QWebServerType createForConfiguration( + final QWebConfiguration configuration) + throws IOException + { + final var serverConfiguration = + configuration.serverConfiguration(); + + final var server = + new QWebServer( + serverConfiguration.hostName(), + serverConfiguration.port() + ); + + server.enableGzip(serverConfiguration.enableGZIP()); + + for (final var rec : configuration.responses()) { + final var r = server.addResponse(); + r.withStatus(rec.statusCode()); + r.withFixedData(rec.content()); + r.withContentLength(rec.content().length); + + for (final var entry : rec.headers().entrySet()) { + r.withHeader(entry.getKey(), entry.getValue()); + } + } + + return server; + } + private record QWebRequestReceived( String method, String path, @@ -152,6 +199,7 @@ private static final class QWebServer extends NanoHTTPD private final LinkedList responses; private final LinkedList requests; private boolean gzipEnabled; + private Consumer callback; QWebServer( final String hostName, @@ -167,6 +215,9 @@ private static final class QWebServer extends NanoHTTPD new LinkedList<>(); this.requests = new LinkedList<>(); + this.callback = + r -> { + }; this.baseURI = URI.create( @@ -193,6 +244,12 @@ public Response serve( this.requests.add(requestReceived); + try { + this.callback.accept(requestReceived); + } catch (final Exception e) { + // Ignored + } + try { session.parseBody(requestReceived.files); } catch (final Exception e) { @@ -274,6 +331,13 @@ public List requestsReceived() { return List.copyOf(this.requests); } + + @Override + public void setRequestCallback( + final Consumer onRequest) + { + this.callback = Objects.requireNonNull(onRequest, "onRequest"); + } } private static final class QMutableResponse implements QWebResponseType diff --git a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/package-info.java b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/package-info.java index 759de2f..72058a7 100644 --- a/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/package-info.java +++ b/com.io7m.quixote.core/src/main/java/com/io7m/quixote/core/package-info.java @@ -19,7 +19,7 @@ */ @Export -@Version("1.1.0") +@Version("1.2.0") package com.io7m.quixote.core; import org.osgi.annotation.bundle.Export; diff --git a/com.io7m.quixote.main/pom.xml b/com.io7m.quixote.main/pom.xml new file mode 100644 index 0000000..c248adb --- /dev/null +++ b/com.io7m.quixote.main/pom.xml @@ -0,0 +1,142 @@ + + + + + 4.0.0 + + + com.io7m.quixote + com.io7m.quixote + 1.2.0-SNAPSHOT + + + com.io7m.quixote.main + + com.io7m.quixote.main + Embedded test suite web server (Main) + https://www.io7m.com/software/quixote + + + + ${project.groupId} + com.io7m.quixote.core + ${project.version} + + + ${project.groupId} + com.io7m.quixote.xml + ${project.version} + + + + com.io7m.blackthorne + com.io7m.blackthorne.core + + + com.io7m.anethum + com.io7m.anethum.slf4j + + + org.slf4j + slf4j-api + + + com.io7m.anethum + com.io7m.anethum.api + + + ch.qos.logback + logback-classic + + + + org.osgi + org.osgi.annotation.bundle + provided + + + org.osgi + org.osgi.annotation.versioning + provided + + + + + + + + + org.cyclonedx + cyclonedx-maven-plugin + + + default + + makeAggregateBom + + prepare-package + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + + ch.qos.logback:logback-classic:* + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + distribution + + single + + package + + + src/main/assembly/distribution.xml + + + + + + + + io.github.zlika + reproducible-build-maven-plugin + + + reproducible-zip + package + + strip-jar + + + true + + .*\.zip + + + + + + + + + diff --git a/com.io7m.quixote.main/src/main/assembly/distribution.xml b/com.io7m.quixote.main/src/main/assembly/distribution.xml new file mode 100644 index 0000000..5c6cf7b --- /dev/null +++ b/com.io7m.quixote.main/src/main/assembly/distribution.xml @@ -0,0 +1,38 @@ + + + + + distribution + + quixote + + + dir + zip + + + + + lib + true + true + + + + + + src/main/sh/quixote + /bin + 0755 + + + ${project.build.directory}/bom.xml + + 0644 + + + + diff --git a/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/Main.java b/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/Main.java new file mode 100644 index 0000000..16b215b --- /dev/null +++ b/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/Main.java @@ -0,0 +1,123 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.main; + +import com.io7m.anethum.slf4j.ParseStatusLogging; +import com.io7m.blackthorne.core.BTPreserveLexical; +import com.io7m.quixote.core.QWebConfiguration; +import com.io7m.quixote.core.QWebRequestLogging; +import com.io7m.quixote.core.QWebServers; +import com.io7m.quixote.xml.QWebConfigurationXML; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Objects; + +/** + * Server main entry point. + */ + +public final class Main +{ + private static final Logger LOG = + LoggerFactory.getLogger(Main.class); + + private static final OpenOption[] OPEN_OPTIONS = { + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.APPEND, + }; + + private Main() + { + + } + + /** + * Server main entry point. + * + * @param args The command-line arguments + * + * @throws Exception On errors + */ + + public static void main( + final String[] args) + throws Exception + { + if (args.length == 1) { + if (Objects.equals(args[0], "help")) { + LOG.info("Self-check succeeded."); + return; + } + } + + if (args.length != 2) { + LOG.info("Usage: input.xml output.bin"); + throw new IllegalArgumentException( + "Missing required command-line arguments."); + } + + final var inputFile = + Paths.get(args[0]).toAbsolutePath(); + final var outputFile = + Paths.get(args[1]).toAbsolutePath(); + + final QWebConfiguration configuration; + try (var stream = Files.newInputStream(inputFile)) { + configuration = + QWebConfigurationXML.parse( + inputFile.toUri(), + stream, + BTPreserveLexical.PRESERVE_LEXICAL_INFORMATION, + status -> { + ParseStatusLogging.logWithAll(LOG, status); + }); + } + + try (var outputLog = + Files.newOutputStream(outputFile, OPEN_OPTIONS)) { + try (var server = + QWebServers.createServerForConfiguration(configuration)) { + + LOG.info("Quixote running at {}", server.uri()); + + server.setRequestCallback(r -> { + try { + QWebRequestLogging.append(outputLog, r); + } catch (final IOException e) { + LOG.error("Failed to write output log: ", e); + } + }); + + while (true) { + try { + Thread.sleep(1_000L); + } catch (final InterruptedException e) { + return; + } + } + } + } + } +} diff --git a/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/package-info.java b/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/package-info.java new file mode 100644 index 0000000..a8a0d2f --- /dev/null +++ b/com.io7m.quixote.main/src/main/java/com/io7m/quixote/main/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (Main) + */ + +@Export +@Version("1.0.0") +package com.io7m.quixote.main; + +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.quixote.main/src/main/java/module-info.java b/com.io7m.quixote.main/src/main/java/module-info.java new file mode 100644 index 0000000..0cd66f9 --- /dev/null +++ b/com.io7m.quixote.main/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (Main) + */ + +module com.io7m.quixote.main +{ + requires static org.osgi.annotation.bundle; + requires static org.osgi.annotation.versioning; + + requires com.io7m.quixote.core; + requires com.io7m.quixote.xml; + + requires com.io7m.anethum.api; + requires com.io7m.anethum.slf4j; + requires com.io7m.blackthorne.core; + requires org.slf4j; + + exports com.io7m.quixote.main; +} diff --git a/com.io7m.quixote.main/src/main/resources/logback.xml b/com.io7m.quixote.main/src/main/resources/logback.xml new file mode 100644 index 0000000..5da69bd --- /dev/null +++ b/com.io7m.quixote.main/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + %level %logger: %msg%n + + System.out + + + + + + + diff --git a/com.io7m.quixote.main/src/main/resources/logback.xsd b/com.io7m.quixote.main/src/main/resources/logback.xsd new file mode 100644 index 0000000..16db5d6 --- /dev/null +++ b/com.io7m.quixote.main/src/main/resources/logback.xsd @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.io7m.quixote.main/src/main/sh/quixote b/com.io7m.quixote.main/src/main/sh/quixote new file mode 100755 index 0000000..223d7cd --- /dev/null +++ b/com.io7m.quixote.main/src/main/sh/quixote @@ -0,0 +1,12 @@ +#!/bin/sh + +if [ -z "${QUIXOTE_HOME}" ] +then + echo "QUIXOTE_HOME is unset" 1>&2 + exit 1 +fi + +exec /usr/bin/env java \ +-p "${QUIXOTE_HOME}/lib" \ +-m com.io7m.quixote.main/com.io7m.quixote.main.Main \ +"$@" diff --git a/com.io7m.quixote.oci/pom.xml b/com.io7m.quixote.oci/pom.xml new file mode 100644 index 0000000..e4c7a32 --- /dev/null +++ b/com.io7m.quixote.oci/pom.xml @@ -0,0 +1,181 @@ + + + + + 4.0.0 + + + com.io7m.quixote + com.io7m.quixote + 1.2.0-SNAPSHOT + + + com.io7m.quixote.oci + + com.io7m.quixote.oci + Embedded test suite web server (OCI image) + https://www.io7m.com/software/quixote + + + 3.18.2 + 21_35-jre-alpine + + + + + ${project.groupId} + com.io7m.quixote.main + ${project.version} + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + com.io7m.quixote:* + ch.qos.logback:logback-classic::* + + + + + + + + + io7m-oci-image + + + + ${project.groupId} + com.io7m.quixote.main + ${project.version} + distribution + zip + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-sources + package + + unpack-dependencies + + + module-info.java + ${project.groupId} + com.io7m.quixote.main + zip + distribution + true + ${project.build.directory}/oci + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-containerfile + package + + copy-resources + + + + + src/main/resources + true + + + ${project.build.directory}/oci + + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + + oci-image-create + + exec + + package + + podman + + build + --timestamp + 1577836800 + -t + quay.io/io7mcom/quixote:${project.version} + ${project.build.directory}/oci + + + + + + + oci-image-run-check + + exec + + package + + podman + + run + --rm + --read-only + quay.io/io7mcom/quixote:${project.version} + help + + + + + + + oci-image-push + + exec + + deploy + + podman + + push + quay.io/io7mcom/quixote:${project.version} + + + + + + + + + + + diff --git a/com.io7m.quixote.oci/src/main/resources/Containerfile b/com.io7m.quixote.oci/src/main/resources/Containerfile new file mode 100644 index 0000000..a6b7276 --- /dev/null +++ b/com.io7m.quixote.oci/src/main/resources/Containerfile @@ -0,0 +1,17 @@ +FROM docker.io/library/eclipse-temurin:${com.io7m.oci.versionTemurin} + +ENV PATH="/quixote/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/java/openjdk/bin" +ENV QUIXOTE_HOME="/quixote" + +COPY quixote /quixote + +LABEL "org.opencontainers.image.authors"="Mark Raynsford" +LABEL "org.opencontainers.image.description"="Embedded test suite web server" +LABEL "org.opencontainers.image.licenses"="ISC" +LABEL "org.opencontainers.image.source"="https://www.github.com/io7m/quixote" +LABEL "org.opencontainers.image.title"="Quixote" +LABEL "org.opencontainers.image.url"="https://www.io7m.com/software/quixote" +LABEL "org.opencontainers.image.version"="${project.version}" +LABEL "org.opencontainers.image.revision"="${buildNumber}" + +ENTRYPOINT ["/quixote/bin/quixote"] diff --git a/com.io7m.quixote.tests/pom.xml b/com.io7m.quixote.tests/pom.xml index be69b01..e014f39 100644 --- a/com.io7m.quixote.tests/pom.xml +++ b/com.io7m.quixote.tests/pom.xml @@ -29,6 +29,16 @@ com.io7m.quixote.core ${project.version} + + ${project.groupId} + com.io7m.quixote.xml + ${project.version} + + + ${project.groupId} + com.io7m.quixote.main + ${project.version} + org.nanohttpd @@ -44,6 +54,19 @@ junit-jupiter-api + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.mockito + mockito-core + + org.osgi org.osgi.annotation.bundle diff --git a/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebConfigurationXMLTest.java b/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebConfigurationXMLTest.java new file mode 100644 index 0000000..181b91b --- /dev/null +++ b/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebConfigurationXMLTest.java @@ -0,0 +1,172 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.tests; + +import com.io7m.anethum.api.ParsingException; +import com.io7m.blackthorne.core.BTPreserveLexical; +import com.io7m.quixote.xml.QWebConfigurationXML; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class QWebConfigurationXMLTest +{ + /** + * Configuration parsing. + * + * @throws Exception On errors + */ + + @Test + public void testConfig0() + throws Exception + { + final var c = + QWebConfigurationXML.parse( + URI.create("urn:stdin"), + resource("conf-0.xml"), + BTPreserveLexical.DISCARD_LEXICAL_INFORMATION, + status -> { + } + ); + + assertEquals(20001, c.serverConfiguration().port()); + assertTrue(c.serverConfiguration().enableGZIP()); + + { + final var r = c.responses().get(0); + assertEquals(200, r.statusCode()); + assertEquals("GET", r.method().pattern()); + assertEquals("/", r.path().pattern()); + assertEquals(Map.ofEntries( + Map.entry("Content-Type", "application/octet-stream") + ), r.headers()); + assertEquals( + new String( + resource("README-LICENSE.txt").readAllBytes(), + StandardCharsets.UTF_8 + ), + new String( + r.content(), + StandardCharsets.UTF_8 + ) + ); + } + + assertEquals(1, c.responses().size()); + } + + /** + * Configuration parsing. + * + * @throws Exception On errors + */ + + @Test + public void testConfig1() + throws Exception + { + final var c = + QWebConfigurationXML.parse( + URI.create("urn:stdin"), + resource("conf-1.xml"), + BTPreserveLexical.DISCARD_LEXICAL_INFORMATION, + status -> { + } + ); + + assertEquals(20001, c.serverConfiguration().port()); + assertTrue(c.serverConfiguration().enableGZIP()); + + { + final var r = c.responses().get(0); + assertEquals(200, r.statusCode()); + assertEquals("GET", r.method().pattern()); + assertEquals("/", r.path().pattern()); + assertEquals(Map.ofEntries( + Map.entry("Content-Type", "application/octet-stream") + ), r.headers()); + assertEquals( + new String( + resource("README-LICENSE.txt").readAllBytes(), + StandardCharsets.UTF_8 + ), + new String( + r.content(), + StandardCharsets.UTF_8 + ) + ); + } + + assertEquals(1, c.responses().size()); + } + + /** + * Configuration parsing. + */ + + @TestFactory + public Stream testParseErrors() + { + return Stream.of( + "conf-error-0.xml", + "conf-error-1.xml" + ).map(QWebConfigurationXMLTest::testParseError); + } + + private static DynamicTest testParseError( + final String name) + { + return DynamicTest.dynamicTest( + "testParseError_%s".formatted(name), + () -> { + Assertions.assertThrows(ParsingException.class, () -> { + QWebConfigurationXML.parse( + URI.create("urn:stdin"), + resource(name), + BTPreserveLexical.DISCARD_LEXICAL_INFORMATION, + status -> { + + } + ); + }); + }); + } + + private static InputStream resource( + final String name) + throws Exception + { + final var path = + "/com/io7m/quixote/tests/%s".formatted(name); + final var url = + QWebConfigurationXMLTest.class.getResource(path); + + return url.openStream(); + } +} diff --git a/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebRequestLoggingTest.java b/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebRequestLoggingTest.java new file mode 100644 index 0000000..62d52a2 --- /dev/null +++ b/com.io7m.quixote.tests/src/main/java/com/io7m/quixote/tests/QWebRequestLoggingTest.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.tests; + +import com.io7m.quixote.core.QWebRequestLogging; +import com.io7m.quixote.core.QWebRequestReceivedType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +public final class QWebRequestLoggingTest +{ + private ByteArrayOutputStream out; + private QWebRequestReceivedType request; + + @BeforeEach + public void setup() + { + this.out = + new ByteArrayOutputStream(); + this.request = + Mockito.mock(QWebRequestReceivedType.class); + } + + @Test + public void testSimple() + throws IOException + { + when(this.request.method()) + .thenReturn("POST"); + when(this.request.path()) + .thenReturn("/x/y/z"); + when(this.request.headers()) + .thenReturn(Map.of()); + when(this.request.files()) + .thenReturn(Map.of()); + + QWebRequestLogging.append(this.out, this.request); + QWebRequestLogging.append(this.out, this.request); + QWebRequestLogging.append(this.out, this.request); + + final var in = + new ByteArrayInputStream(this.out.toByteArray()); + + { + final var r = QWebRequestLogging.read(in); + assertEquals("/x/y/z", r.path()); + assertEquals("POST", r.method()); + assertEquals(Map.of(), r.headers()); + assertEquals(Map.of(), r.files()); + } + + { + final var r = QWebRequestLogging.read(in); + assertEquals("/x/y/z", r.path()); + assertEquals("POST", r.method()); + assertEquals(Map.of(), r.headers()); + assertEquals(Map.of(), r.files()); + } + + { + final var r = QWebRequestLogging.read(in); + assertEquals("/x/y/z", r.path()); + assertEquals("POST", r.method()); + assertEquals(Map.of(), r.headers()); + assertEquals(Map.of(), r.files()); + } + } +} diff --git a/com.io7m.quixote.tests/src/main/java/module-info.java b/com.io7m.quixote.tests/src/main/java/module-info.java index 39a0dfb..ff1b297 100644 --- a/com.io7m.quixote.tests/src/main/java/module-info.java +++ b/com.io7m.quixote.tests/src/main/java/module-info.java @@ -26,6 +26,12 @@ requires org.junit.platform.engine; requires com.io7m.quixote.core; + requires com.io7m.quixote.xml; + + requires org.mockito; + requires com.io7m.anethum.slf4j; + requires com.io7m.anethum.api; + requires com.io7m.blackthorne.core; requires java.net.http; exports com.io7m.quixote.tests; diff --git a/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/README-LICENSE.txt b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/README-LICENSE.txt new file mode 100644 index 0000000..8872798 --- /dev/null +++ b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/README-LICENSE.txt @@ -0,0 +1,13 @@ +Copyright © 2024 Mark Raynsford https://www.io7m.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-0.xml b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-0.xml new file mode 100644 index 0000000..17786b4 --- /dev/null +++ b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-0.xml @@ -0,0 +1,32 @@ + + + + + + +
+ + + Q29weXJpZ2h0IMKpIDIwMjQgTWFyayBSYXluc2ZvcmQgPGNvZGVAaW83bS5jb20+IGh0dHBzOi8v + d3d3LmlvN20uY29tCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlz + dHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkKcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVl + IGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZQpjb3B5cmlnaHQgbm90 + aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhF + IFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIFRIRSBBVVRIT1IgRElTQ0xBSU1TIEFM + TCBXQVJSQU5USUVTCldJVEggUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJ + TVBMSUVEIFdBUlJBTlRJRVMgT0YKTUVSQ0hBTlRBQklMSVRZIEFORCBGSVRORVNTLiBJTiBOTyBF + VkVOVCBTSEFMTCBUSEUgQVVUSE9SIEJFIExJQUJMRSBGT1IKQU5ZIFNQRUNJQUwsIERJUkVDVCwg + SU5ESVJFQ1QsIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyBPUiBBTlkgREFNQUdFUwpXSEFUU09F + VkVSIFJFU1VMVElORyBGUk9NIExPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIg + SU4gQU4KQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SIE9USEVSIFRPUlRJT1VTIEFD + VElPTiwgQVJJU0lORyBPVVQgT0YKT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBVU0UgT1IgUEVS + Rk9STUFOQ0UgT0YgVEhJUyBTT0ZUV0FSRS4K + + + + diff --git a/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-1.xml b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-1.xml new file mode 100644 index 0000000..d0a9741 --- /dev/null +++ b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-1.xml @@ -0,0 +1,30 @@ + + + + + + +
+ + https://www.io7m.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +]]> + + + diff --git a/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-0.xml b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-0.xml new file mode 100644 index 0000000..1d2af5f --- /dev/null +++ b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-0.xml @@ -0,0 +1,17 @@ + + + + + + +
+ + + + + diff --git a/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-1.xml b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-1.xml new file mode 100644 index 0000000..600a624 --- /dev/null +++ b/com.io7m.quixote.tests/src/main/resources/com/io7m/quixote/tests/conf-error-1.xml @@ -0,0 +1 @@ +not even XML! \ No newline at end of file diff --git a/com.io7m.quixote.xml/pom.xml b/com.io7m.quixote.xml/pom.xml new file mode 100644 index 0000000..1d79591 --- /dev/null +++ b/com.io7m.quixote.xml/pom.xml @@ -0,0 +1,61 @@ + + + + + 4.0.0 + + + com.io7m.quixote + com.io7m.quixote + 1.2.0-SNAPSHOT + + + com.io7m.quixote.xml + + com.io7m.quixote.xml + Embedded test suite web server (XML) + https://www.io7m.com/software/quixote + + + + ${project.groupId} + com.io7m.quixote.core + ${project.version} + + + + com.io7m.jlexing + com.io7m.jlexing.core + + + com.io7m.blackthorne + com.io7m.blackthorne.core + + + com.io7m.blackthorne + com.io7m.blackthorne.jxe + + + com.io7m.jxe + com.io7m.jxe.core + + + com.io7m.anethum + com.io7m.anethum.api + + + + org.osgi + org.osgi.annotation.bundle + provided + + + org.osgi + org.osgi.annotation.versioning + provided + + + + diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebConfigurationXML.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebConfigurationXML.java new file mode 100644 index 0000000..c3754e3 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebConfigurationXML.java @@ -0,0 +1,177 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml; + +import com.io7m.anethum.api.ParseSeverity; +import com.io7m.anethum.api.ParseStatus; +import com.io7m.anethum.api.ParsingException; +import com.io7m.anethum.api.SerializationException; +import com.io7m.blackthorne.core.BTException; +import com.io7m.blackthorne.core.BTParseError; +import com.io7m.blackthorne.core.BTPreserveLexical; +import com.io7m.blackthorne.jxe.BlackthorneJXE; +import com.io7m.quixote.core.QWebConfiguration; +import com.io7m.quixote.xml.v1.QWX1; +import com.io7m.quixote.xml.v1.QWX1File; +import com.io7m.quixote.xml.v1.QWX1Serializer; + +import javax.xml.stream.XMLStreamException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Functions to handle XML configurations. + */ + +public final class QWebConfigurationXML +{ + private static final OpenOption[] OPEN_OPTIONS = { + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + }; + + private QWebConfigurationXML() + { + + } + + /** + * Parse a configuration file. + * + * @param source The source + * @param stream The stream + * @param preserveLexical The lexical preservation flag + * @param statusConsumer The status consume + * + * @return A parsed configuration + * + * @throws ParsingException On errors + */ + + public static QWebConfiguration parse( + final URI source, + final InputStream stream, + final BTPreserveLexical preserveLexical, + final Consumer statusConsumer) + throws ParsingException + { + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(stream, "stream"); + Objects.requireNonNull(preserveLexical, "preserveLexical"); + Objects.requireNonNull(statusConsumer, "statusConsumer"); + + try { + return BlackthorneJXE.parse( + source, + stream, + Map.ofEntries( + Map.entry( + QWX1.element("Configuration"), + QWX1File::new + ) + ), + QWebSchemas.schemas(), + preserveLexical + ); + } catch (final BTException e) { + final var statuses = + e.errors() + .stream() + .map(QWebConfigurationXML::mapParseError) + .toList(); + + for (final var status : statuses) { + statusConsumer.accept(status); + } + + throw new ParsingException(e.getMessage(), List.copyOf(statuses)); + } + } + + /** + * Serialize a configuration file. + * + * @param stream The output stream + * @param configuration The server configuration + * + * @throws SerializationException On errors + */ + + public static void serialize( + final OutputStream stream, + final QWebConfiguration configuration) + throws SerializationException + { + try { + new QWX1Serializer(stream).execute(configuration); + } catch (final XMLStreamException e) { + throw new SerializationException(e.getMessage(), e); + } + } + + /** + * Serialize a configuration file. + * + * @param file The output file + * @param configuration The server configuration + * + * @throws SerializationException On errors + */ + + public static void serialize( + final Path file, + final QWebConfiguration configuration) + throws SerializationException + { + try (var output = Files.newOutputStream(file, OPEN_OPTIONS)) { + serialize(output, configuration); + output.flush(); + } catch (final IOException e) { + throw new SerializationException(e.getMessage(), e); + } + } + + private static ParseStatus mapParseError( + final BTParseError error) + { + return ParseStatus.builder("parse-error", error.message()) + .withSeverity(mapSeverity(error.severity())) + .withLexical(error.lexical()) + .build(); + } + + private static ParseSeverity mapSeverity( + final BTParseError.Severity severity) + { + return switch (severity) { + case ERROR -> ParseSeverity.PARSE_ERROR; + case WARNING -> ParseSeverity.PARSE_WARNING; + }; + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebSchemas.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebSchemas.java new file mode 100644 index 0000000..3cb8f3d --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/QWebSchemas.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2023 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml; + +import com.io7m.jxe.core.JXESchemaDefinition; +import com.io7m.jxe.core.JXESchemaResolutionMappings; + +import java.net.URI; + +/** + * Configuration XML schemas. + */ + +public final class QWebSchemas +{ + private static final JXESchemaDefinition SCHEMA_1 = + JXESchemaDefinition.builder() + .setFileIdentifier("configuration-1.xsd") + .setLocation(QWebSchemas.class.getResource( + "/com/io7m/quixote/xml/configuration-1.xsd")) + .setNamespace(URI.create("urn:com.io7m.quixote:configuration:1")) + .build(); + + private static final JXESchemaResolutionMappings SCHEMA_MAPPINGS = + JXESchemaResolutionMappings.builder() + .putMappings(SCHEMA_1.namespace(), SCHEMA_1) + .build(); + + /** + * @return The v1 schema + */ + + public static JXESchemaDefinition schema1() + { + return SCHEMA_1; + } + + /** + * @return The set of supported schemas. + */ + + public static JXESchemaResolutionMappings schemas() + { + return SCHEMA_MAPPINGS; + } + + private QWebSchemas() + { + + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/package-info.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/package-info.java new file mode 100644 index 0000000..aac8926 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (XML) + */ + +@Export +@Version("1.0.0") +package com.io7m.quixote.xml; + +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1.java new file mode 100644 index 0000000..5c704de --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2023 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTQualifiedName; +import com.io7m.quixote.xml.QWebSchemas; + +/** + * Functions over v1 elements. + */ + +public final class QWX1 +{ + private QWX1() + { + + } + + /** + * The element with the given name. + * + * @param localName The local name + * + * @return The qualified name + */ + + public static BTQualifiedName element( + final String localName) + { + return BTQualifiedName.of( + QWebSchemas.schema1().namespace().toString(), + localName + ); + } + +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentBase64.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentBase64.java new file mode 100644 index 0000000..ee329c4 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentBase64.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; + +import java.io.ByteArrayOutputStream; +import java.util.Base64; +import java.util.regex.Pattern; + +/** + * XML element handler. + */ + +public final class QWX1ContentBase64 + implements BTElementHandlerType +{ + private static final Pattern WHITESPACE = + Pattern.compile("\\s+"); + + private ByteArrayOutputStream bytes; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1ContentBase64( + final BTElementParsingContextType context) + { + this.bytes = new ByteArrayOutputStream(); + } + + @Override + public void onCharacters( + final BTElementParsingContextType context, + final char[] data, + final int offset, + final int length) + { + final var rawText = + new String(data, offset, length); + final var trimmedText = + WHITESPACE.matcher(rawText).replaceAll(""); + final var decoded = + Base64.getDecoder() + .decode(trimmedText); + + this.bytes.writeBytes(decoded); + } + + @Override + public byte[] onElementFinished( + final BTElementParsingContextType context) + { + return this.bytes.toByteArray(); + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentUTF8.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentUTF8.java new file mode 100644 index 0000000..81dd5e2 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1ContentUTF8.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; + +/** + * XML element handler. + */ + +public final class QWX1ContentUTF8 + implements BTElementHandlerType +{ + private String result; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1ContentUTF8( + final BTElementParsingContextType context) + { + + } + + @Override + public void onCharacters( + final BTElementParsingContextType context, + final char[] data, + final int offset, + final int length) + { + this.result = new String(data, offset, length); + } + + @Override + public String onElementFinished( + final BTElementParsingContextType context) + { + return this.result; + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1File.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1File.java new file mode 100644 index 0000000..1eed54d --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1File.java @@ -0,0 +1,137 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerConstructorType; +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; +import com.io7m.blackthorne.core.BTIgnoreUnrecognizedElements; +import com.io7m.blackthorne.core.BTQualifiedName; +import com.io7m.blackthorne.core.Blackthorne; +import com.io7m.quixote.core.QWebConfiguration; +import com.io7m.quixote.core.QWebResponseRecorded; +import com.io7m.quixote.core.QWebServerConfiguration; +import org.xml.sax.Attributes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * XML element handler. + */ + +public final class QWX1File + implements BTElementHandlerType +{ + private List responses; + private int port; + private boolean gzip; + private String hostname; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1File( + final BTElementParsingContextType context) + { + this.responses = + new ArrayList<>(); + } + + @Override + public Map> + onChildHandlersRequested( + final BTElementParsingContextType context) + { + return Map.ofEntries( + Map.entry( + QWX1.element("Responses"), + Blackthorne.forListMono( + QWX1.element("Responses"), + QWX1.element("Response"), + QWX1Response::new, + BTIgnoreUnrecognizedElements.DO_NOT_IGNORE_UNRECOGNIZED_ELEMENTS + ) + ) + ); + } + + @Override + public void onElementStart( + final BTElementParsingContextType context, + final Attributes attributes) + { + this.hostname = + Objects.requireNonNullElse( + attributes.getValue("HostName"), + "localhost" + ); + this.port = + Integer.parseUnsignedInt(attributes.getValue("Port")); + this.gzip = + Boolean.parseBoolean(attributes.getValue("GZIP")); + } + + @Override + public void onChildValueProduced( + final BTElementParsingContextType context, + final Object result) + { + switch (result) { + case final List responseList -> { + if (!responseList.isEmpty()) { + final var r = responseList.get(0); + switch (r) { + case final QWebResponseRecorded rec -> { + this.responses.addAll( + (Collection) responseList + ); + } + default -> { + throw new IllegalStateException("Unexpected value: " + r); + } + } + } + } + + default -> { + throw new IllegalStateException("Unexpected value: " + result); + } + } + } + + @Override + public QWebConfiguration onElementFinished( + final BTElementParsingContextType context) + { + return new QWebConfiguration( + new QWebServerConfiguration( + this.hostname, + this.port, + this.gzip + ), + this.responses + ); + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Header.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Header.java new file mode 100644 index 0000000..81cba7b --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Header.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; +import org.xml.sax.Attributes; + +import java.util.Map; + +/** + * XML element handler. + */ + +public final class QWX1Header + implements BTElementHandlerType> +{ + private Map.Entry entry; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1Header( + final BTElementParsingContextType context) + { + + } + + @Override + public void onElementStart( + final BTElementParsingContextType context, + final Attributes attributes) + { + this.entry = Map.entry( + attributes.getValue("Name"), + attributes.getValue("Value") + ); + } + + @Override + public Map.Entry onElementFinished( + final BTElementParsingContextType context) + { + return this.entry; + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Headers.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Headers.java new file mode 100644 index 0000000..583b2c2 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Headers.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerConstructorType; +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; +import com.io7m.blackthorne.core.BTQualifiedName; + +import java.util.HashMap; +import java.util.Map; + +/** + * XML element handler. + */ + +public final class QWX1Headers + implements BTElementHandlerType, Map> +{ + private final HashMap entries; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1Headers( + final BTElementParsingContextType context) + { + this.entries = new HashMap<>(); + } + + @Override + public Map< + BTQualifiedName, + BTElementHandlerConstructorType>> + onChildHandlersRequested( + final BTElementParsingContextType context) + { + return Map.ofEntries( + Map.entry( + QWX1.element("Header"), + QWX1Header::new + ) + ); + } + + @Override + public void onChildValueProduced( + final BTElementParsingContextType context, + final Map.Entry result) + { + this.entries.put(result.getKey(), result.getValue()); + } + + @Override + public Map onElementFinished( + final BTElementParsingContextType context) + { + return Map.copyOf(this.entries); + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Response.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Response.java new file mode 100644 index 0000000..008b756 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Response.java @@ -0,0 +1,150 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.blackthorne.core.BTElementHandlerConstructorType; +import com.io7m.blackthorne.core.BTElementHandlerType; +import com.io7m.blackthorne.core.BTElementParsingContextType; +import com.io7m.blackthorne.core.BTQualifiedName; +import com.io7m.quixote.core.QWebResponseRecorded; +import org.xml.sax.Attributes; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * XML element handler. + */ + +public final class QWX1Response + implements BTElementHandlerType +{ + private final Map headers; + private Pattern path; + private byte[] content; + private Pattern method; + private int statusCode; + + /** + * XML element handler. + * + * @param context The parse context + */ + + public QWX1Response( + final BTElementParsingContextType context) + { + this.headers = new HashMap<>(); + this.headers.put("Content-Type", "application/octet-stream"); + + this.statusCode = + 200; + this.content = + new byte[0]; + this.method = + Pattern.compile(".*", Pattern.CASE_INSENSITIVE); + this.path = + Pattern.compile("^/.*", Pattern.CASE_INSENSITIVE); + } + + @Override + public Map> + onChildHandlersRequested( + final BTElementParsingContextType context) + { + return Map.ofEntries( + Map.entry( + QWX1.element("Headers"), + QWX1Headers::new + ), + Map.entry( + QWX1.element("ContentBase64"), + QWX1ContentBase64::new + ), + Map.entry( + QWX1.element("ContentUTF8"), + QWX1ContentUTF8::new + ) + ); + } + + @Override + public void onChildValueProduced( + final BTElementParsingContextType context, + final Object result) + { + switch (result) { + case final Map m -> { + this.headers.putAll((Map) m); + } + + case final String data -> { + this.content = data.getBytes(StandardCharsets.UTF_8); + } + + case final byte[] data -> { + this.content = data; + } + + default -> { + throw new IllegalStateException("Unexpected value: " + result); + } + } + } + + @Override + public void onElementStart( + final BTElementParsingContextType context, + final Attributes attributes) + { + this.method = + Pattern.compile( + Objects.requireNonNullElse( + attributes.getValue("Method"), + ".*" + ), + Pattern.CASE_INSENSITIVE + ); + this.path = + Pattern.compile( + Objects.requireNonNullElse( + attributes.getValue("Path"), + "^/.*" + ), + Pattern.CASE_INSENSITIVE + ); + this.statusCode = + Integer.parseUnsignedInt(attributes.getValue("Status")); + } + + @Override + public QWebResponseRecorded onElementFinished( + final BTElementParsingContextType context) + { + return new QWebResponseRecorded( + this.method, + this.path, + this.statusCode, + this.headers, + this.content + ); + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Serializer.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Serializer.java new file mode 100644 index 0000000..091ba90 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/QWX1Serializer.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.quixote.xml.v1; + +import com.io7m.quixote.core.QWebConfiguration; +import com.io7m.quixote.core.QWebResponseRecorded; +import com.io7m.quixote.xml.QWebSchemas; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.OutputStream; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +/** + * A serializer for configurations. + */ + +public final class QWX1Serializer +{ + private final XMLStreamWriter output; + private final XMLOutputFactory outputs; + private final String ns; + + /** + * A serializer for configurations. + * + * @param inOutput The output stream + * + * @throws XMLStreamException On errors + */ + + public QWX1Serializer( + final OutputStream inOutput) + throws XMLStreamException + { + this.outputs = + XMLOutputFactory.newFactory(); + this.output = + this.outputs.createXMLStreamWriter(inOutput, "UTF-8"); + this.ns = + QWebSchemas.schema1().namespace().toString(); + } + + /** + * Execute the serializer. + * + * @param configuration The configuration + * + * @throws XMLStreamException On errors + */ + + public void execute( + final QWebConfiguration configuration) + throws XMLStreamException + { + this.output.writeStartDocument("UTF-8", "1.0"); + this.serializeConfiguration(configuration); + this.output.writeEndDocument(); + } + + private void serializeConfiguration( + final QWebConfiguration configuration) + throws XMLStreamException + { + this.output.writeStartElement("Configuration"); + this.output.writeDefaultNamespace(this.ns); + + this.output.writeAttribute( + "HostName", + configuration.serverConfiguration().hostName() + ); + this.output.writeAttribute( + "Port", + Integer.toUnsignedString(configuration.serverConfiguration().port()) + ); + this.output.writeAttribute( + "GZIP", + Boolean.toString(configuration.serverConfiguration().enableGZIP()) + ); + + this.serializeResponses(configuration.responses()); + this.output.writeEndElement(); + } + + private void serializeResponses( + final List responses) + throws XMLStreamException + { + this.output.writeStartElement("Responses"); + + for (final var response : responses) { + this.serializeResponse(response); + } + + this.output.writeEndElement(); + } + + private void serializeResponse( + final QWebResponseRecorded response) + throws XMLStreamException + { + this.output.writeStartElement("Response"); + + this.output.writeAttribute( + "Path", + response.path().pattern()); + this.output.writeAttribute( + "Method", + response.method().pattern()); + this.output.writeAttribute( + "Status", + Integer.toUnsignedString(response.statusCode())); + + this.serializeHeaders(response.headers()); + this.serializeContent(response.content()); + + this.output.writeEndElement(); + } + + private void serializeContent( + final byte[] content) + throws XMLStreamException + { + this.output.writeStartElement("ContentBase64"); + this.output.writeCData(Base64.getEncoder().encodeToString(content)); + this.output.writeEndElement(); + } + + private void serializeHeaders( + final Map headers) + throws XMLStreamException + { + this.output.writeStartElement("Headers"); + + final var keys = new TreeSet<>(headers.keySet()); + for (final var key : keys) { + final var value = headers.get(key); + this.output.writeStartElement("Header"); + this.output.writeAttribute("Name", key); + this.output.writeAttribute("Value", value); + this.output.writeEndElement(); + } + + this.output.writeEndElement(); + } +} diff --git a/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/package-info.java b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/package-info.java new file mode 100644 index 0000000..ec8fcb9 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/com/io7m/quixote/xml/v1/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (XML) + */ + +@Version("1.0.0") +package com.io7m.quixote.xml.v1; + +import org.osgi.annotation.versioning.Version; diff --git a/com.io7m.quixote.xml/src/main/java/module-info.java b/com.io7m.quixote.xml/src/main/java/module-info.java new file mode 100644 index 0000000..ae12568 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/java/module-info.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Embedded test suite web server (XML) + */ + +module com.io7m.quixote.xml +{ + requires static org.osgi.annotation.bundle; + requires static org.osgi.annotation.versioning; + + requires com.io7m.anethum.api; + requires com.io7m.blackthorne.core; + requires com.io7m.blackthorne.jxe; + requires com.io7m.jxe.core; + requires com.io7m.quixote.core; + + exports com.io7m.quixote.xml; +} diff --git a/com.io7m.quixote.xml/src/main/resources/com/io7m/quixote/xml/configuration-1.xsd b/com.io7m.quixote.xml/src/main/resources/com/io7m/quixote/xml/configuration-1.xsd new file mode 100644 index 0000000..f230c90 --- /dev/null +++ b/com.io7m.quixote.xml/src/main/resources/com/io7m/quixote/xml/configuration-1.xsd @@ -0,0 +1,180 @@ + + + + + + + + + + The content returned with a response. + + + + + + + + + + + + + + The content returned with a response. + + + + + + + + + + + + + + A header that will be returned by a response. + + + + + + + + The header name. + + + + + + + + The header value. + + + + + + + + + + The headers that will be returned by a response. + + + + + + + + + + + + + + A response that will be returned. + + + + + + + + + + + + + + + + The HTTP method against which to match this response. + + + + + + + + The path against which to match this response. + + + + + + + + The status code returned by this response. + + + + + + + + + + + + + + + + + + + + + + + + The hostname to which to bind the server. + + + + + + + + The port to which to bind the server. + + + + + + + + Whether to enable GZIP. + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index c299846..d3ab925 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,10 @@ com.io7m.quixote.core + com.io7m.quixote.main + com.io7m.quixote.oci com.io7m.quixote.tests + com.io7m.quixote.xml @@ -137,6 +140,41 @@ nanohttpd 2.3.1 + + com.io7m.blackthorne + com.io7m.blackthorne.core + 2.0.0 + + + com.io7m.blackthorne + com.io7m.blackthorne.jxe + 2.0.0 + + + com.io7m.jxe + com.io7m.jxe.core + 1.0.2 + + + com.io7m.anethum + com.io7m.anethum.api + 1.1.0 + + + com.io7m.anethum + com.io7m.anethum.slf4j + 1.1.0 + + + com.io7m.jlexing + com.io7m.jlexing.core + 3.1.0 + + + org.mockito + mockito-core + 5.11.0 +