001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020
021import java.nio.file.Path;
022
023import static java.util.Objects.requireNonNull;
024
025/**
026 * A known-length, finalized HTTP response body.
027 * <p>
028 * This type describes the body to write; it is not itself responsible for writing to a transport. Bodies may be backed
029 * by bytes, files, file channels, or byte buffers.
030 *
031 * @author <a href="https://www.revetkn.com">Mark Allen</a>
032 */
033public sealed interface MarshaledResponseBody permits MarshaledResponseBody.Bytes, MarshaledResponseBody.File, MarshaledResponseBody.FileChannel, MarshaledResponseBody.ByteBuffer {
034        /**
035         * The number of bytes this body will write.
036         *
037         * @return the body length
038         */
039        @NonNull
040        Long getLength();
041
042        /**
043         * A finalized response body backed by a byte array.
044         *
045         * @author <a href="https://www.revetkn.com">Mark Allen</a>
046         */
047        final class Bytes implements MarshaledResponseBody {
048                @NonNull
049                private final byte[] bytes;
050
051                public Bytes(@NonNull byte[] bytes) {
052                        this.bytes = requireNonNull(bytes);
053                }
054
055                /**
056                 * The byte array backing this body.
057                 * <p>
058                 * For compatibility with prior {@link MarshaledResponse} behavior, this array is not defensively copied.
059                 *
060                 * @return the bytes to write
061                 */
062                @NonNull
063                public byte[] getBytes() {
064                        return this.bytes;
065                }
066
067                @Override
068                @NonNull
069                public Long getLength() {
070                        return Long.valueOf(getBytes().length);
071                }
072        }
073
074        /**
075         * A finalized response body backed by a file path.
076         *
077         * @author <a href="https://www.revetkn.com">Mark Allen</a>
078         */
079        final class File implements MarshaledResponseBody {
080                @NonNull
081                private final Path path;
082                @NonNull
083                private final Long offset;
084                @NonNull
085                private final Long count;
086
087                public File(@NonNull Path path, @NonNull Long offset, @NonNull Long count) {
088                        this.path = requireNonNull(path);
089                        this.offset = requireNonNull(offset);
090                        this.count = requireNonNull(count);
091                        validateOffsetAndCount(offset, count);
092                }
093
094                /**
095                 * The file path backing this body.
096                 *
097                 * @return the file path
098                 */
099                @NonNull
100                public Path getPath() {
101                        return this.path;
102                }
103
104                /**
105                 * The zero-based file offset from which response bytes should be written.
106                 *
107                 * @return the file offset
108                 */
109                @NonNull
110                public Long getOffset() {
111                        return this.offset;
112                }
113
114                /**
115                 * The number of file bytes to write.
116                 *
117                 * @return the byte count
118                 */
119                @NonNull
120                public Long getCount() {
121                        return this.count;
122                }
123
124                @Override
125                @NonNull
126                public Long getLength() {
127                        return getCount();
128                }
129        }
130
131        /**
132         * A finalized response body backed by a {@link java.nio.channels.FileChannel}.
133         *
134         * @author <a href="https://www.revetkn.com">Mark Allen</a>
135         */
136        final class FileChannel implements MarshaledResponseBody {
137                private final java.nio.channels.@NonNull FileChannel channel;
138                @NonNull
139                private final Long offset;
140                @NonNull
141                private final Long count;
142                @NonNull
143                private final Boolean closeOnComplete;
144
145                public FileChannel(java.nio.channels.@NonNull FileChannel channel,
146                                                                                         @NonNull Long offset,
147                                                                                         @NonNull Long count,
148                                                                                         @NonNull Boolean closeOnComplete) {
149                        this.channel = requireNonNull(channel);
150                        this.offset = requireNonNull(offset);
151                        this.count = requireNonNull(count);
152                        this.closeOnComplete = requireNonNull(closeOnComplete);
153                        validateOffsetAndCount(offset, count);
154                }
155
156                /**
157                 * The file channel backing this body.
158                 *
159                 * @return the file channel
160                 */
161                public java.nio.channels.@NonNull FileChannel getChannel() {
162                        return this.channel;
163                }
164
165                /**
166                 * The zero-based channel offset from which response bytes should be written.
167                 *
168                 * @return the channel offset
169                 */
170                @NonNull
171                public Long getOffset() {
172                        return this.offset;
173                }
174
175                /**
176                 * The number of channel bytes to write.
177                 *
178                 * @return the byte count
179                 */
180                @NonNull
181                public Long getCount() {
182                        return this.count;
183                }
184
185                /**
186                 * Whether Soklet should close this caller-supplied channel after the response completes or fails.
187                 *
188                 * @return {@code true} if Soklet should close the channel
189                 */
190                @NonNull
191                public Boolean getCloseOnComplete() {
192                        return this.closeOnComplete;
193                }
194
195                @Override
196                @NonNull
197                public Long getLength() {
198                        return getCount();
199                }
200        }
201
202        /**
203         * A finalized response body backed by a {@link java.nio.ByteBuffer}.
204         * <p>
205         * The buffer's position and limit at construction time define the response slice.
206         *
207         * @author <a href="https://www.revetkn.com">Mark Allen</a>
208         */
209        final class ByteBuffer implements MarshaledResponseBody {
210                private final java.nio.@NonNull ByteBuffer buffer;
211
212                public ByteBuffer(java.nio.@NonNull ByteBuffer buffer) {
213                        this.buffer = requireNonNull(buffer).slice().asReadOnlyBuffer();
214                }
215
216                /**
217                 * The read-only buffer slice backing this body.
218                 *
219                 * @return a read-only duplicate of the response buffer
220                 */
221                public java.nio.@NonNull ByteBuffer getBuffer() {
222                        return this.buffer.asReadOnlyBuffer();
223                }
224
225                @Override
226                @NonNull
227                public Long getLength() {
228                        return Long.valueOf(this.buffer.remaining());
229                }
230        }
231
232        private static void validateOffsetAndCount(@NonNull Long offset, @NonNull Long count) {
233                requireNonNull(offset);
234                requireNonNull(count);
235
236                if (offset < 0)
237                        throw new IllegalArgumentException("Offset must be >= 0.");
238
239                if (count < 0)
240                        throw new IllegalArgumentException("Count must be >= 0.");
241
242                if (Long.MAX_VALUE - offset < count)
243                        throw new IllegalArgumentException("Offset plus count exceeds maximum supported file position.");
244        }
245}