diff --git a/docs/asciidoc/udp-client.adoc b/docs/asciidoc/udp-client.adoc index 9e040de1dc..8c6a60cc90 100644 --- a/docs/asciidoc/udp-client.adoc +++ b/docs/asciidoc/udp-client.adoc @@ -271,3 +271,17 @@ include::{examplesdir}/metrics/custom/Application.java[lines=18..37] ---- <1> Enables UDP client metrics and provides {javadoc}/reactor/netty/channel/ChannelMetricsRecorder.html[`ChannelMetricsRecorder`] implementation. ==== + +== Unix Domain Sockets +The `UdpClient` supports Unix Domain Datagram Sockets (UDS) when native transport is in use. + +The following example shows how to use UDS support: + +==== +[source,java,indent=0] +.{examplesdir}/uds/Application.java +---- +include::{examplesdir}/uds/Application.java[lines=18..42] +---- +<1> Specifies `DomainSocketAddress` that will be used +==== diff --git a/docs/asciidoc/udp-server.adoc b/docs/asciidoc/udp-server.adoc index 831eb5f4b7..486e9f5c4d 100644 --- a/docs/asciidoc/udp-server.adoc +++ b/docs/asciidoc/udp-server.adoc @@ -263,3 +263,17 @@ include::{examplesdir}/metrics/custom/Application.java[lines=18..35] ---- <1> Enables UDP server metrics and provides {javadoc}/reactor/netty/channel/ChannelMetricsRecorder.html[`ChannelMetricsRecorder`] implementation. ==== + +== Unix Domain Sockets +The `UdpServer` supports Unix Domain Datagram Sockets (UDS) when native transport is in use. + +The following example shows how to use UDS support: + +==== +[source,java,indent=0] +.{examplesdir}/uds/Application.java +---- +include::{examplesdir}/uds/Application.java[lines=18..48] +---- +<1> Specifies `DomainSocketAddress` that will be used +==== diff --git a/reactor-netty-core/src/main/java/reactor/netty/DisposableChannel.java b/reactor-netty-core/src/main/java/reactor/netty/DisposableChannel.java index e686f71f1a..432f4171bc 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/DisposableChannel.java +++ b/reactor-netty-core/src/main/java/reactor/netty/DisposableChannel.java @@ -20,6 +20,7 @@ import io.netty.channel.Channel; import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.unix.DomainDatagramChannel; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.publisher.Mono; @@ -44,7 +45,7 @@ public interface DisposableChannel extends Disposable { */ default SocketAddress address() { Channel c = channel(); - if (c instanceof DatagramChannel) { + if (c instanceof DatagramChannel || c instanceof DomainDatagramChannel) { SocketAddress a = c.remoteAddress(); return a != null ? a : c.localAddress(); } diff --git a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopEpoll.java b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopEpoll.java index a8b2bbb2cd..f74c4f0623 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopEpoll.java +++ b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopEpoll.java @@ -21,6 +21,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollDomainDatagramChannel; import io.netty.channel.epoll.EpollDomainSocketChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerDomainSocketChannel; @@ -29,6 +30,7 @@ import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; +import io.netty.channel.unix.DomainDatagramChannel; import io.netty.channel.unix.DomainSocketChannel; import io.netty.channel.unix.ServerDomainSocketChannel; import reactor.util.Logger; @@ -60,6 +62,9 @@ public CHANNEL getChannel(Class channelClass) if (channelClass.equals(ServerDomainSocketChannel.class)) { return (CHANNEL) new EpollServerDomainSocketChannel(); } + if (channelClass.equals(DomainDatagramChannel.class)) { + return (CHANNEL) new EpollDomainDatagramChannel(); + } throw new IllegalArgumentException("Unsupported channel type: " + channelClass.getSimpleName()); } diff --git a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopKQueue.java b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopKQueue.java index 5b26cd5273..8f1d06f357 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopKQueue.java +++ b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopKQueue.java @@ -21,6 +21,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueDatagramChannel; +import io.netty.channel.kqueue.KQueueDomainDatagramChannel; import io.netty.channel.kqueue.KQueueDomainSocketChannel; import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueServerDomainSocketChannel; @@ -29,6 +30,7 @@ import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; +import io.netty.channel.unix.DomainDatagramChannel; import io.netty.channel.unix.DomainSocketChannel; import io.netty.channel.unix.ServerDomainSocketChannel; import reactor.util.Logger; @@ -59,6 +61,9 @@ public CHANNEL getChannel(Class channelClass) if (channelClass.equals(ServerDomainSocketChannel.class)) { return (CHANNEL) new KQueueServerDomainSocketChannel(); } + if (channelClass.equals(DomainDatagramChannel.class)) { + return (CHANNEL) new KQueueDomainDatagramChannel(); + } throw new IllegalArgumentException("Unsupported channel type: " + channelClass.getSimpleName()); } diff --git a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpClientConfig.java b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpClientConfig.java index 87fc6811b2..bbe0aa9427 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpClientConfig.java +++ b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpClientConfig.java @@ -22,6 +22,7 @@ import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.unix.DomainDatagramChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.resolver.AddressResolverGroup; @@ -79,19 +80,13 @@ public final InternetProtocolFamily family() { @Override protected Class channelType(boolean isDomainSocket) { - if (isDomainSocket) { - throw new UnsupportedOperationException(); - } - return DatagramChannel.class; + return isDomainSocket ? DomainDatagramChannel.class : DatagramChannel.class; } @Override protected ChannelFactory connectionFactory(EventLoopGroup elg, boolean isDomainSocket) { - if (isDomainSocket) { - throw new UnsupportedOperationException(); - } if (isPreferNative()) { - return () -> loopResources().onChannel(DatagramChannel.class, elg); + return super.connectionFactory(elg, isDomainSocket); } else { return () -> new NioDatagramChannel(family()); diff --git a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpConnection.java b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpConnection.java index 526f65c9d0..b7ec82b903 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpConnection.java +++ b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpConnection.java @@ -32,6 +32,7 @@ interface UdpConnection { * @param multicastAddress multicast address of the group to join * * @return a {@link Publisher} that will be complete when the group has been joined + * @throws UnsupportedOperationException when Unix Domain Sockets */ default Mono join(InetAddress multicastAddress) { return join(multicastAddress, null); @@ -43,6 +44,7 @@ default Mono join(InetAddress multicastAddress) { * @param multicastAddress multicast address of the group to join * * @return a {@link Publisher} that will be complete when the group has been joined + * @throws UnsupportedOperationException when Unix Domain Sockets */ Mono join(final InetAddress multicastAddress, @Nullable NetworkInterface iface); @@ -52,6 +54,7 @@ default Mono join(InetAddress multicastAddress) { * @param multicastAddress multicast address of the group to leave * * @return a {@link Publisher} that will be complete when the group has been left + * @throws UnsupportedOperationException when Unix Domain Sockets */ default Mono leave(InetAddress multicastAddress) { return leave(multicastAddress, null); @@ -63,6 +66,7 @@ default Mono leave(InetAddress multicastAddress) { * @param multicastAddress multicast address of the group to leave * * @return a {@link Publisher} that will be complete when the group has been left + * @throws UnsupportedOperationException when Unix Domain Sockets */ Mono leave(final InetAddress multicastAddress, @Nullable NetworkInterface iface); } diff --git a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpOperations.java b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpOperations.java index 8888c25d5e..0a4a6c2054 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpOperations.java +++ b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpOperations.java @@ -39,11 +39,8 @@ final class UdpOperations extends ChannelOperations implements UdpInbound, UdpOutbound { - final DatagramChannel datagramChannel; - UdpOperations(Connection c, ConnectionObserver listener) { super(c, listener); - this.datagramChannel = (DatagramChannel) c.channel(); } /** @@ -55,6 +52,10 @@ final class UdpOperations extends ChannelOperations */ @Override public Mono join(final InetAddress multicastAddress, @Nullable NetworkInterface iface) { + if (!(connection().channel() instanceof DatagramChannel)) { + throw new UnsupportedOperationException(); + } + DatagramChannel datagramChannel = (DatagramChannel) connection().channel(); if (null == iface && null != datagramChannel.config().getNetworkInterface()) { iface = datagramChannel.config().getNetworkInterface(); } @@ -86,6 +87,10 @@ public Mono join(final InetAddress multicastAddress, @Nullable NetworkInte */ @Override public Mono leave(final InetAddress multicastAddress, @Nullable NetworkInterface iface) { + if (!(connection().channel() instanceof DatagramChannel)) { + throw new UnsupportedOperationException(); + } + DatagramChannel datagramChannel = (DatagramChannel) connection().channel(); if (null == iface && null != datagramChannel.config().getNetworkInterface()) { iface = datagramChannel.config().getNetworkInterface(); } diff --git a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpServerConfig.java b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpServerConfig.java index fd273befd2..e2a70582b7 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/udp/UdpServerConfig.java +++ b/reactor-netty-core/src/main/java/reactor/netty/udp/UdpServerConfig.java @@ -23,6 +23,7 @@ import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.unix.DomainDatagramChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import reactor.netty.ChannelPipelineConfigurer; @@ -117,19 +118,13 @@ public final InternetProtocolFamily family() { @Override protected Class channelType(boolean isDomainSocket) { - if (isDomainSocket) { - throw new UnsupportedOperationException(); - } - return DatagramChannel.class; + return isDomainSocket ? DomainDatagramChannel.class : DatagramChannel.class; } @Override protected ChannelFactory connectionFactory(EventLoopGroup elg, boolean isDomainSocket) { - if (isDomainSocket) { - throw new UnsupportedOperationException(); - } if (isPreferNative()) { - return () -> loopResources().onChannel(DatagramChannel.class, elg); + return super.connectionFactory(elg, isDomainSocket); } else { return () -> new NioDatagramChannel(family()); diff --git a/reactor-netty-core/src/test/java/reactor/netty/udp/UdpClientTest.java b/reactor-netty-core/src/test/java/reactor/netty/udp/UdpClientTest.java index 4085eaf456..aa37fc2f41 100644 --- a/reactor-netty-core/src/test/java/reactor/netty/udp/UdpClientTest.java +++ b/reactor-netty-core/src/test/java/reactor/netty/udp/UdpClientTest.java @@ -15,7 +15,10 @@ */ package reactor.netty.udp; +import java.io.File; +import java.io.FileNotFoundException; import java.net.InetSocketAddress; +import java.nio.file.Files; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -24,15 +27,18 @@ import io.netty.buffer.Unpooled; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.unix.DomainDatagramPacket; import io.netty.channel.unix.DomainSocketAddress; import io.netty.util.CharsetUtil; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.resources.LoopResources; +import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assumptions.assumeThat; class UdpClientTest { @@ -139,14 +145,6 @@ void testIssue192() throws Exception { } } - @Test - void testUdpClientWithDomainSockets() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> UdpClient.create() - .remoteAddress(() -> new DomainSocketAddress("/tmp/test.sock")) - .connectNow()); - } - @Test void testUdpClientWithDomainSocketsWithHost() { assertThatExceptionOfType(IllegalArgumentException.class) @@ -164,4 +162,130 @@ void testUdpClientWithDomainSocketsWithPort() { .port(1234) .connectNow()); } + + @Test + void testUdpClientWithDomainSocketsNIOTransport() { + LoopResources loop = LoopResources.create("testUdpClientWithDomainSocketsNIOTransport"); + try { + UdpClient.create() + .runOn(loop, false) + .remoteAddress(() -> new DomainSocketAddress("/tmp/test.sock")) + .connect() + .as(StepVerifier::create) + .expectError(IllegalArgumentException.class) + .verify(Duration.ofSeconds(5)); + } + finally { + loop.disposeLater() + .block(Duration.ofSeconds(30)); + } + } + + @Test + void testUdpClientWithDomainSocketsConnectionRefused() { + assumeThat(LoopResources.hasNativeSupport()).isTrue(); + UdpClient.create() + .remoteAddress(() -> new DomainSocketAddress("/tmp/test.sock")) + .connect() + .as(StepVerifier::create) + .expectError(FileNotFoundException.class) + .verify(Duration.ofSeconds(5)); + } + + @Test + void domainSocketsSmokeTest() throws Exception { + assumeThat(LoopResources.hasNativeSupport()).isTrue(); + LoopResources resources = LoopResources.create("domainSocketsSmokeTest"); + CountDownLatch latch = new CountDownLatch(4); + Connection server = null; + Connection client1 = null; + Connection client2 = null; + try { + server = + UdpServer.create() + .bindAddress(UdpClientTest::newDomainSocketAddress) + .runOn(resources) + .handle((in, out) -> in.receiveObject() + .map(o -> { + if (o instanceof DomainDatagramPacket) { + DomainDatagramPacket received = (DomainDatagramPacket) o; + ByteBuf buffer = received.content(); + System.out.println("Server received " + + buffer.readCharSequence(buffer.readableBytes(), CharsetUtil.UTF_8)); + ByteBuf buf1 = Unpooled.copiedBuffer("echo ", CharsetUtil.UTF_8); + ByteBuf buf2 = Unpooled.copiedBuffer(buf1, buffer); + buf1.release(); + return new DomainDatagramPacket(buf2, received.sender()); + } + else { + return Mono.error(new Exception()); + } + }) + .flatMap(out::sendObject)) + .wiretap(true) + .bind() + .block(Duration.ofSeconds(30)); + assertThat(server).isNotNull(); + + DomainSocketAddress address = (DomainSocketAddress) server.address(); + client1 = + UdpClient.create() + .bindAddress(UdpClientTest::newDomainSocketAddress) + .remoteAddress(() -> address) + .runOn(resources) + .handle((in, out) -> { + in.receive() + .subscribe(b -> latch.countDown()); + return out.sendString(Mono.just("ping1")) + .then(out.sendString(Mono.just("ping2"))) + .neverComplete(); + }) + .wiretap(true) + .connect() + .block(Duration.ofSeconds(30)); + assertThat(client1).isNotNull(); + + client2 = + UdpClient.create() + .bindAddress(UdpClientTest::newDomainSocketAddress) + .remoteAddress(() -> address) + .runOn(resources) + .handle((in, out) -> { + in.receive() + .subscribe(b -> latch.countDown()); + return out.sendString(Mono.just("ping3")) + .then(out.sendString(Mono.just("ping4"))) + .neverComplete(); + }) + .wiretap(true) + .connect() + .block(Duration.ofSeconds(30)); + assertThat(client2).isNotNull(); + + assertThat(latch.await(30, TimeUnit.SECONDS)).as("latch await").isTrue(); + } + finally { + if (server != null) { + server.disposeNow(); + } + if (client1 != null) { + client1.disposeNow(); + } + if (client2 != null) { + client2.disposeNow(); + } + } + } + + private static DomainSocketAddress newDomainSocketAddress() { + try { + File tempFile = Files.createTempFile("UdpClientTest", "UDS").toFile(); + assertThat(tempFile.delete()).isTrue(); + tempFile.deleteOnExit(); + return new DomainSocketAddress(tempFile); + } + catch (Exception e) { + throw new RuntimeException("Error creating a temporary file", e); + } + } } diff --git a/reactor-netty-core/src/test/java/reactor/netty/udp/UdpServerTests.java b/reactor-netty-core/src/test/java/reactor/netty/udp/UdpServerTests.java index 93c39762bf..037846b86b 100644 --- a/reactor-netty-core/src/test/java/reactor/netty/udp/UdpServerTests.java +++ b/reactor-netty-core/src/test/java/reactor/netty/udp/UdpServerTests.java @@ -48,6 +48,7 @@ import reactor.netty.Connection; import reactor.netty.SocketUtils; import reactor.netty.resources.LoopResources; +import reactor.test.StepVerifier; import reactor.util.Logger; import reactor.util.Loggers; @@ -273,14 +274,6 @@ void portBindingException() { conn.disposeNow(); } - @Test - void testUdpServerWithDomainSockets() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> UdpServer.create() - .bindAddress(() -> new DomainSocketAddress("/tmp/test.sock")) - .bindNow()); - } - @Test void testUdpServerWithDomainSocketsWithHost() { assertThatExceptionOfType(IllegalArgumentException.class) @@ -306,4 +299,22 @@ void testBindTimeoutLongOverflow() { .port(0) .bindNow(Duration.ofMillis(Long.MAX_VALUE))); } + + @Test + void testUdpServerWithDomainSocketsNIOTransport() { + LoopResources loop = LoopResources.create("testUdpServerWithDomainSocketsNIOTransport"); + try { + UdpServer.create() + .runOn(loop, false) + .bindAddress(() -> new DomainSocketAddress("/tmp/test.sock")) + .bind() + .as(StepVerifier::create) + .expectError(IllegalArgumentException.class) + .verify(Duration.ofSeconds(5)); + } + finally { + loop.disposeLater() + .block(Duration.ofSeconds(30)); + } + } } diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/client/uds/Application.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/client/uds/Application.java new file mode 100644 index 0000000000..1081ed30ae --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/client/uds/Application.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed 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 + * + * https://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 reactor.netty.examples.documentation.udp.client.uds; + +import io.netty.channel.unix.DomainSocketAddress; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.udp.UdpClient; + +import java.io.File; + +public class Application { + + public static void main(String[] args) { + Connection connection = + UdpClient.create() + .bindAddress(Application::newDomainSocketAddress) + .remoteAddress(() -> new DomainSocketAddress("/tmp/test-server.sock")) //<1> + .handle((in, out) -> + out.sendString(Mono.just("hello")) + .then(in.receive() + .asString() + .doOnNext(System.out::println) + .then())) + .connectNow(); + + connection.onDispose() + .block(); + } + + private static DomainSocketAddress newDomainSocketAddress() { + try { + File tempFile = new File("/tmp/test-client.sock"); + tempFile.delete(); + tempFile.deleteOnExit(); + return new DomainSocketAddress(tempFile); + } + catch (Exception e) { + throw new RuntimeException("Error creating a temporary file", e); + } + } +} \ No newline at end of file diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/server/uds/Application.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/server/uds/Application.java new file mode 100644 index 0000000000..f5fb13eaad --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/udp/server/uds/Application.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed 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 + * + * https://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 reactor.netty.examples.documentation.udp.server.uds; + +import io.netty.channel.unix.DomainDatagramPacket; +import io.netty.channel.unix.DomainSocketAddress; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.udp.UdpServer; + +import java.io.File; + +public class Application { + + public static void main(String[] args) { + Connection server = + UdpServer.create() + .bindAddress(Application::newDomainSocketAddress) //<1> + .handle((in, out) -> + out.sendObject( + in.receiveObject() + .map(o -> { + if (o instanceof DomainDatagramPacket) { + DomainDatagramPacket p = (DomainDatagramPacket) o; + return new DomainDatagramPacket(p.content().retain(), p.sender()); + } + else { + return Mono.error(new Exception("Unexpected type of the message: " + o)); + } + }))) + .bindNow(); + + server.onDispose() + .block(); + } + + private static DomainSocketAddress newDomainSocketAddress() { + try { + File tempFile = new File("/tmp/test-server.sock"); + tempFile.delete(); + tempFile.deleteOnExit(); + return new DomainSocketAddress(tempFile); + } + catch (Exception e) { + throw new RuntimeException("Error creating a temporary file", e); + } + } +} \ No newline at end of file