001/*
002 * Copyright 2022-2025 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 com.soklet.internal.spring.LinkedCaseInsensitiveMap;
020
021import javax.annotation.Nonnull;
022import javax.annotation.Nullable;
023import javax.annotation.concurrent.NotThreadSafe;
024import javax.annotation.concurrent.ThreadSafe;
025import java.util.LinkedHashSet;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.Optional;
029import java.util.Set;
030import java.util.function.Consumer;
031
032import static java.lang.String.format;
033import static java.util.Objects.requireNonNull;
034
035/**
036 * A finalized representation of a {@link Response}, suitable for sending to clients over the wire.
037 * <p>
038 * Your application's {@link ResponseMarshaler} is responsible for taking the {@link Response} returned by a <em>Resource Method</em> as input
039 * and converting its {@link Response#getBody()} to a {@code byte[]}.
040 * <p>
041 * For example, if a {@link Response} were to specify a body of {@code List.of("one", "two")}, a {@link ResponseMarshaler} might
042 * convert it to the JSON string {@code ["one", "two"]} and provide as output a corresponding {@link MarshaledResponse} with a body of UTF-8 bytes that represent {@code ["one", "two"]}.
043 * <p>
044 * Alternatively, your <em>Resource Method</em> might want to directly serve bytes to clients (e.g. an image or PDF) and skip the {@link ResponseMarshaler} entirely.
045 * To accomplish this, just have your <em>Resource Method</em> return a {@link MarshaledResponse} instance: this tells Soklet "I already know exactly what bytes I want to send; don't go through the normal marshaling process".
046 * <p>
047 * Instances can be acquired via the {@link #withResponse(Response)} or {@link #withStatusCode(Integer)} builder factory methods.
048 * <p>
049 * Full documentation is available at <a href="https://www.soklet.com/docs/response-writing">https://www.soklet.com/docs/response-writing</a>.
050 *
051 * @author <a href="https://www.revetkn.com">Mark Allen</a>
052 */
053@ThreadSafe
054public final class MarshaledResponse {
055        @Nonnull
056        private final Integer statusCode;
057        @Nonnull
058        private final Map<String, Set<String>> headers;
059        @Nonnull
060        private final Set<ResponseCookie> cookies;
061        @Nullable
062        private final byte[] body;
063
064        /**
065         * Acquires a builder for {@link MarshaledResponse} instances.
066         *
067         * @param response the logical response whose values are used to prime this builder
068         * @return the builder
069         */
070        @Nonnull
071        public static Builder withResponse(@Nonnull Response response) {
072                requireNonNull(response);
073
074                Object rawBody = response.getBody().orElse(null);
075                byte[] body = null;
076
077                if (rawBody != null && rawBody instanceof byte[] byteArrayBody)
078                        body = byteArrayBody;
079
080                return new Builder(response.getStatusCode())
081                                .headers(response.getHeaders())
082                                .cookies(response.getCookies())
083                                .body(body);
084        }
085
086        /**
087         * Acquires a builder for {@link MarshaledResponse} instances.
088         *
089         * @param statusCode the HTTP status code for this response
090         * @return the builder
091         */
092        @Nonnull
093        public static Builder withStatusCode(@Nonnull Integer statusCode) {
094                requireNonNull(statusCode);
095                return new Builder(statusCode);
096        }
097
098        /**
099         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
100         *
101         * @return a copier for this instance
102         */
103        @Nonnull
104        public Copier copy() {
105                return new Copier(this);
106        }
107
108        protected MarshaledResponse(@Nonnull Builder builder) {
109                requireNonNull(builder);
110
111                this.statusCode = builder.statusCode;
112                this.headers = builder.headers == null ? Map.of() : new LinkedCaseInsensitiveMap<>(builder.headers);
113                this.cookies = builder.cookies == null ? Set.of() : new LinkedHashSet<>(builder.cookies);
114                this.body = builder.body;
115
116                // Verify headers are legal
117                for (Entry<String, Set<String>> entry : this.headers.entrySet()) {
118                        String headerName = entry.getKey();
119                        Set<String> headerValues = entry.getValue();
120
121                        for (String headerValue : headerValues)
122                                Utilities.validateHeaderNameAndValue(headerName, headerValue);
123                }
124        }
125
126        @Override
127        public String toString() {
128                return format("%s{statusCode=%s, headers=%s, cookies=%s, body=%s}", getClass().getSimpleName(),
129                                getStatusCode(), getHeaders(), getCookies(),
130                                format("%d bytes", getBody().isPresent() ? getBody().get().length : 0));
131        }
132
133        /**
134         * The HTTP status code for this response.
135         *
136         * @return the status code
137         */
138        @Nonnull
139        public Integer getStatusCode() {
140                return this.statusCode;
141        }
142
143        /**
144         * The HTTP headers to write for this response.
145         *
146         * @return the headers to write
147         */
148        @Nonnull
149        public Map<String, Set<String>> getHeaders() {
150                return this.headers;
151        }
152
153        /**
154         * The HTTP cookies to write for this response.
155         *
156         * @return the cookies to write
157         */
158        @Nonnull
159        public Set<ResponseCookie> getCookies() {
160                return this.cookies;
161        }
162
163        /**
164         * The HTTP response body to write, if available.
165         *
166         * @return the response body to write, or {@link Optional#empty()}) if no body should be written
167         */
168        @Nonnull
169        public Optional<byte[]> getBody() {
170                return Optional.ofNullable(this.body);
171        }
172
173        /**
174         * Builder used to construct instances of {@link MarshaledResponse} via {@link MarshaledResponse#withResponse(Response)} or {@link MarshaledResponse#withStatusCode(Integer)}.
175         * <p>
176         * This class is intended for use by a single thread.
177         *
178         * @author <a href="https://www.revetkn.com">Mark Allen</a>
179         */
180        @NotThreadSafe
181        public static final class Builder {
182                @Nonnull
183                private Integer statusCode;
184                @Nullable
185                private Set<ResponseCookie> cookies;
186                @Nullable
187                private Map<String, Set<String>> headers;
188                @Nullable
189                private byte[] body;
190
191                protected Builder(@Nonnull Integer statusCode) {
192                        requireNonNull(statusCode);
193                        this.statusCode = statusCode;
194                }
195
196                @Nonnull
197                public Builder statusCode(@Nonnull Integer statusCode) {
198                        requireNonNull(statusCode);
199                        this.statusCode = statusCode;
200                        return this;
201                }
202
203                @Nonnull
204                public Builder cookies(@Nullable Set<ResponseCookie> cookies) {
205                        this.cookies = cookies;
206                        return this;
207                }
208
209                @Nonnull
210                public Builder headers(@Nullable Map<String, Set<String>> headers) {
211                        this.headers = headers;
212                        return this;
213                }
214
215                @Nonnull
216                public Builder body(@Nullable byte[] body) {
217                        this.body = body;
218                        return this;
219                }
220
221                @Nonnull
222                public MarshaledResponse build() {
223                        return new MarshaledResponse(this);
224                }
225        }
226
227        /**
228         * Builder used to copy instances of {@link MarshaledResponse} via {@link MarshaledResponse#copy()}.
229         * <p>
230         * This class is intended for use by a single thread.
231         *
232         * @author <a href="https://www.revetkn.com">Mark Allen</a>
233         */
234        @NotThreadSafe
235        public static final class Copier {
236                @Nonnull
237                private final Builder builder;
238
239                Copier(@Nonnull MarshaledResponse marshaledResponse) {
240                        requireNonNull(marshaledResponse);
241
242                        this.builder = new Builder(marshaledResponse.getStatusCode())
243                                        .headers(new LinkedCaseInsensitiveMap<>(marshaledResponse.getHeaders()))
244                                        .cookies(new LinkedHashSet<>(marshaledResponse.getCookies()))
245                                        .body(marshaledResponse.getBody().orElse(null));
246                }
247
248                @Nonnull
249                public Copier statusCode(@Nonnull Integer statusCode) {
250                        requireNonNull(statusCode);
251                        this.builder.statusCode(statusCode);
252                        return this;
253                }
254
255                @Nonnull
256                public Copier headers(@Nonnull Map<String, Set<String>> headers) {
257                        this.builder.headers(headers);
258                        return this;
259                }
260
261                // Convenience method for mutation
262                @Nonnull
263                public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) {
264                        requireNonNull(headersConsumer);
265
266                        if (this.builder.headers == null)
267                                this.builder.headers(new LinkedCaseInsensitiveMap<>());
268
269                        headersConsumer.accept(this.builder.headers);
270                        return this;
271                }
272
273                @Nonnull
274                public Copier cookies(@Nullable Set<ResponseCookie> cookies) {
275                        this.builder.cookies(cookies);
276                        return this;
277                }
278
279                // Convenience method for mutation
280                @Nonnull
281                public Copier cookies(@Nonnull Consumer<Set<ResponseCookie>> cookiesConsumer) {
282                        requireNonNull(cookiesConsumer);
283
284                        if (this.builder.cookies == null)
285                                this.builder.cookies(new LinkedHashSet<>());
286
287                        cookiesConsumer.accept(this.builder.cookies);
288                        return this;
289                }
290
291                @Nonnull
292                public Copier body(@Nullable byte[] body) {
293                        this.builder.body(body);
294                        return this;
295                }
296
297                @Nonnull
298                public MarshaledResponse finish() {
299                        return this.builder.build();
300                }
301        }
302}