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.core;
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.Collections;
026import java.util.LinkedHashSet;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.Set;
031import java.util.function.Consumer;
032
033import static java.lang.String.format;
034import static java.util.Objects.requireNonNull;
035
036/**
037 * Represents a logical HTTP response returned by a <em>Resource Method</em>.
038 * <p>
039 * Your application's {@link ResponseMarshaler} is responsible for taking the {@link Response} returned by a <em>Resource Method</em> as input
040 * and creating a finalized binary representation ({@link MarshaledResponse}), suitable for sending to clients over the wire.
041 * <p>
042 * Full documentation is available at <a href="https://www.soklet.com/docs/response-writing">https://www.soklet.com/docs/response-writing</a>.
043 *
044 * @author <a href="https://www.revetkn.com">Mark Allen</a>
045 */
046@ThreadSafe
047public class Response {
048        @Nonnull
049        private final Integer statusCode;
050        @Nonnull
051        private final Set<ResponseCookie> cookies;
052        @Nonnull
053        private final Map<String, Set<String>> headers;
054        @Nullable
055        private final Object body;
056
057        /**
058         * Acquires a builder for {@link Response} instances.
059         *
060         * @param statusCode the HTTP status code for this request ({@code 200, 201, etc.})
061         * @return the builder
062         */
063        @Nonnull
064        public static Builder withStatusCode(@Nonnull Integer statusCode) {
065                requireNonNull(statusCode);
066                return new Builder(statusCode);
067        }
068
069        /**
070         * Acquires a builder for {@link Response} instances that are intended to redirect the client.
071         *
072         * @param redirectType the kind of redirect to perform, for example {@link RedirectType#HTTP_307_TEMPORARY_REDIRECT}
073         * @param location     the URL to redirect to
074         * @return the builder
075         */
076        @Nonnull
077        public static Builder withRedirect(@Nonnull RedirectType redirectType,
078                                                                                                                                                 @Nonnull String location) {
079                requireNonNull(redirectType);
080                requireNonNull(location);
081                return new Builder(redirectType, location);
082        }
083
084        protected Response(@Nonnull Builder builder) {
085                requireNonNull(builder);
086
087                Map<String, Set<String>> headers = builder.headers == null
088                                ? new LinkedCaseInsensitiveMap<>()
089                                : new LinkedCaseInsensitiveMap<>(builder.headers);
090
091                if (builder.location != null && !headers.containsKey("Location"))
092                        headers.put("Location", Set.of(builder.location));
093
094                Set<ResponseCookie> cookies = builder.cookies == null
095                                ? Collections.emptySet()
096                                : new LinkedHashSet<>(builder.cookies);
097
098                this.statusCode = builder.statusCode;
099                this.cookies = Collections.unmodifiableSet(cookies);
100                this.headers = Collections.unmodifiableMap(headers);
101                this.body = builder.body;
102        }
103
104        @Override
105        public String toString() {
106                return format("%s{statusCode=%s, cookies=%s, headers=%s, body=%s}",
107                                getClass().getSimpleName(), getStatusCode(), getCookies(), getHeaders(), getBody());
108        }
109
110        @Override
111        public boolean equals(@Nullable Object object) {
112                if (this == object)
113                        return true;
114
115                if (!(object instanceof Response response))
116                        return false;
117
118                return Objects.equals(getStatusCode(), response.getStatusCode())
119                                && Objects.equals(getCookies(), response.getCookies())
120                                && Objects.equals(getHeaders(), response.getHeaders())
121                                && Objects.equals(getBody(), response.getBody());
122        }
123
124        @Override
125        public int hashCode() {
126                return Objects.hash(getStatusCode(), getCookies(), getHeaders(), getBody());
127        }
128
129        /**
130         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
131         *
132         * @return a copier for this instance
133         */
134        @Nonnull
135        public Copier copy() {
136                return new Copier(this);
137        }
138
139        /**
140         * The HTTP status code to be written to the client for this response.
141         * <p>
142         * See {@link StatusCode} for an enumeration of all HTTP status codes.
143         *
144         * @return the HTTP status code to write to the response
145         */
146        @Nonnull
147        public Integer getStatusCode() {
148                return this.statusCode;
149        }
150
151        /**
152         * The cookies to be written to the client for this response.
153         * <p>
154         * It is possible to send multiple {@code ResponseCookie} values with the same name to the client.
155         * <p>
156         * <em>Note that {@code ResponseCookie} values, like all response headers, have case-insensitive names per the HTTP spec.</em>
157         *
158         * @return the cookies to write to the response
159         */
160        @Nonnull
161        public Set<ResponseCookie> getCookies() {
162                return this.cookies;
163        }
164
165        /**
166         * The headers to be written to the client for this response.
167         * <p>
168         * The keys are the header names and the values are header values
169         * (it is possible to send the client multiple headers with the same name).
170         * <p>
171         * <em>Note that response headers have case-insensitive names per the HTTP spec.</em>
172         *
173         * @return the headers to write to the response
174         */
175        @Nonnull
176        public Map<String, Set<String>> getHeaders() {
177                return this.headers;
178        }
179
180        /**
181         * The "logical" body content to be written to the response, if present.
182         * <p>
183         * It is the responsibility of the {@link ResponseMarshaler} to take this object and convert it into bytes to send over the wire.
184         *
185         * @return the object representing the response body, or {@link Optional#empty()} if no response body should be written
186         */
187        @Nonnull
188        public Optional<Object> getBody() {
189                return Optional.ofNullable(this.body);
190        }
191
192        /**
193         * Builder used to construct instances of {@link Response} via {@link Response#withStatusCode(Integer)}
194         * or {@link Response#withRedirect(RedirectType, String)}.
195         * <p>
196         * This class is intended for use by a single thread.
197         *
198         * @author <a href="https://www.revetkn.com">Mark Allen</a>
199         */
200        @NotThreadSafe
201        public static class Builder {
202                @Nonnull
203                private Integer statusCode;
204                @Nullable
205                private String location;
206                @Nullable
207                private Set<ResponseCookie> cookies;
208                @Nullable
209                private Map<String, Set<String>> headers;
210                @Nullable
211                private Object body;
212
213                protected Builder(@Nonnull Integer statusCode) {
214                        requireNonNull(statusCode);
215
216                        this.statusCode = statusCode;
217                        this.location = null;
218                }
219
220                protected Builder(@Nonnull RedirectType redirectType,
221                                                                                        @Nonnull String location) {
222                        requireNonNull(redirectType);
223                        requireNonNull(location);
224
225                        this.statusCode = redirectType.getStatusCode().getStatusCode();
226                        this.location = location;
227                }
228
229                @Nonnull
230                public Builder statusCode(@Nonnull Integer statusCode) {
231                        requireNonNull(statusCode);
232
233                        this.statusCode = statusCode;
234                        this.location = null;
235                        return this;
236                }
237
238                @Nonnull
239                public Builder redirect(@Nonnull RedirectType redirectType,
240                                                                                                                @Nonnull String location) {
241                        requireNonNull(redirectType);
242                        requireNonNull(location);
243
244                        this.statusCode = redirectType.getStatusCode().getStatusCode();
245                        this.location = location;
246                        return this;
247                }
248
249                @Nonnull
250                public Builder cookies(@Nullable Set<ResponseCookie> cookies) {
251                        this.cookies = cookies;
252                        return this;
253                }
254
255                @Nonnull
256                public Builder headers(@Nullable Map<String, Set<String>> headers) {
257                        this.headers = headers;
258                        return this;
259                }
260
261                @Nonnull
262                public Builder body(@Nullable Object body) {
263                        this.body = body;
264                        return this;
265                }
266
267                @Nonnull
268                public Response build() {
269                        return new Response(this);
270                }
271        }
272
273        /**
274         * Builder used to copy instances of {@link Response} via {@link Response#copy()}.
275         * <p>
276         * This class is intended for use by a single thread.
277         *
278         * @author <a href="https://www.revetkn.com">Mark Allen</a>
279         */
280        @NotThreadSafe
281        public static class Copier {
282                @Nonnull
283                private final Builder builder;
284
285                Copier(@Nonnull Response response) {
286                        requireNonNull(response);
287
288                        this.builder = new Builder(response.getStatusCode())
289                                        .headers(new LinkedCaseInsensitiveMap<>(response.getHeaders()))
290                                        .cookies(new LinkedHashSet<>(response.getCookies()))
291                                        .body(response.getBody().orElse(null));
292                }
293
294                @Nonnull
295                public Copier statusCode(@Nonnull Integer statusCode) {
296                        requireNonNull(statusCode);
297                        this.builder.statusCode(statusCode);
298                        return this;
299                }
300
301                @Nonnull
302                public Copier headers(@Nullable Map<String, Set<String>> headers) {
303                        this.builder.headers(headers);
304                        return this;
305                }
306
307                // Convenience method for mutation
308                @Nonnull
309                public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) {
310                        requireNonNull(headersConsumer);
311
312                        if (this.builder.headers == null)
313                                this.builder.headers(new LinkedCaseInsensitiveMap<>());
314
315                        headersConsumer.accept(this.builder.headers);
316                        return this;
317                }
318
319                @Nonnull
320                public Copier cookies(@Nullable Set<ResponseCookie> cookies) {
321                        this.builder.cookies(cookies);
322                        return this;
323                }
324
325                // Convenience method for mutation
326                @Nonnull
327                public Copier cookies(@Nonnull Consumer<Set<ResponseCookie>> cookiesConsumer) {
328                        requireNonNull(cookiesConsumer);
329
330                        if (this.builder.cookies == null)
331                                this.builder.cookies(new LinkedHashSet<>());
332
333                        cookiesConsumer.accept(this.builder.cookies);
334                        return this;
335                }
336
337                @Nonnull
338                public Copier body(@Nullable Object body) {
339                        this.builder.body(body);
340                        return this;
341                }
342
343                @Nonnull
344                public Response finish() {
345                        return this.builder.build();
346                }
347        }
348}