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 com.soklet.internal.spring.LinkedCaseInsensitiveMap; 020import org.jspecify.annotations.NonNull; 021import org.jspecify.annotations.Nullable; 022 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 * Convenience instance factories are also available via {@link #fromResponse(Response)} and {@link #fromStatusCode(Integer)}. 049 * <p> 050 * Full documentation is available at <a href="https://www.soklet.com/docs/response-writing">https://www.soklet.com/docs/response-writing</a>. 051 * 052 * @author <a href="https://www.revetkn.com">Mark Allen</a> 053 */ 054@ThreadSafe 055public final class MarshaledResponse { 056 @NonNull 057 private final Integer statusCode; 058 @NonNull 059 private final Map<@NonNull String, @NonNull Set<@NonNull String>> headers; 060 @NonNull 061 private final Set<@NonNull ResponseCookie> cookies; 062 @Nullable 063 private final byte[] body; 064 065 /** 066 * Acquires a builder for {@link MarshaledResponse} instances. 067 * 068 * @param response the logical response whose values are used to prime this builder 069 * @return the builder 070 */ 071 @NonNull 072 public static Builder withResponse(@NonNull Response response) { 073 requireNonNull(response); 074 075 Object rawBody = response.getBody().orElse(null); 076 byte[] body = null; 077 078 if (rawBody != null && rawBody instanceof byte[] byteArrayBody) 079 body = byteArrayBody; 080 081 return new Builder(response.getStatusCode()) 082 .headers(response.getHeaders()) 083 .cookies(response.getCookies()) 084 .body(body); 085 } 086 087 /** 088 * Creates a {@link MarshaledResponse} from a logical {@link Response} without additional customization. 089 * 090 * @param response the logical response whose values are used to construct this instance 091 * @return a {@link MarshaledResponse} instance 092 */ 093 @NonNull 094 public static MarshaledResponse fromResponse(@NonNull Response response) { 095 return withResponse(response).build(); 096 } 097 098 /** 099 * Acquires a builder for {@link MarshaledResponse} instances. 100 * 101 * @param statusCode the HTTP status code for this response 102 * @return the builder 103 */ 104 @NonNull 105 public static Builder withStatusCode(@NonNull Integer statusCode) { 106 requireNonNull(statusCode); 107 return new Builder(statusCode); 108 } 109 110 /** 111 * Creates a {@link MarshaledResponse} with the given status code and no additional customization. 112 * 113 * @param statusCode the HTTP status code for this response 114 * @return a {@link MarshaledResponse} instance 115 */ 116 @NonNull 117 public static MarshaledResponse fromStatusCode(@NonNull Integer statusCode) { 118 return withStatusCode(statusCode).build(); 119 } 120 121 /** 122 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 123 * 124 * @return a copier for this instance 125 */ 126 @NonNull 127 public Copier copy() { 128 return new Copier(this); 129 } 130 131 protected MarshaledResponse(@NonNull Builder builder) { 132 requireNonNull(builder); 133 134 this.statusCode = builder.statusCode; 135 this.headers = builder.headers == null ? Map.of() : new LinkedCaseInsensitiveMap<>(builder.headers); 136 this.cookies = builder.cookies == null ? Set.of() : new LinkedHashSet<>(builder.cookies); 137 this.body = builder.body; 138 139 // Verify headers are legal 140 for (Entry<String, Set<String>> entry : this.headers.entrySet()) { 141 String headerName = entry.getKey(); 142 Set<String> headerValues = entry.getValue(); 143 144 for (String headerValue : headerValues) 145 Utilities.validateHeaderNameAndValue(headerName, headerValue); 146 } 147 } 148 149 @Override 150 public String toString() { 151 return format("%s{statusCode=%s, headers=%s, cookies=%s, body=%s}", getClass().getSimpleName(), 152 getStatusCode(), getHeaders(), getCookies(), 153 format("%d bytes", getBody().isPresent() ? getBody().get().length : 0)); 154 } 155 156 /** 157 * The HTTP status code for this response. 158 * 159 * @return the status code 160 */ 161 @NonNull 162 public Integer getStatusCode() { 163 return this.statusCode; 164 } 165 166 /** 167 * The HTTP headers to write for this response. 168 * <p> 169 * Soklet writes one header line per value. If order matters, provide either a {@link java.util.SortedSet} or 170 * {@link java.util.LinkedHashSet} to preserve the desired ordering; otherwise values are naturally sorted for consistency. 171 * 172 * @return the headers to write 173 */ 174 @NonNull 175 public Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() { 176 return this.headers; 177 } 178 179 /** 180 * The HTTP cookies to write for this response. 181 * 182 * @return the cookies to write 183 */ 184 @NonNull 185 public Set<@NonNull ResponseCookie> getCookies() { 186 return this.cookies; 187 } 188 189 /** 190 * The HTTP response body to write, if available. 191 * 192 * @return the response body to write, or {@link Optional#empty()}) if no body should be written 193 */ 194 @NonNull 195 public Optional<byte[]> getBody() { 196 return Optional.ofNullable(this.body); 197 } 198 199 /** 200 * Builder used to construct instances of {@link MarshaledResponse} via {@link MarshaledResponse#withResponse(Response)} or {@link MarshaledResponse#withStatusCode(Integer)}. 201 * <p> 202 * This class is intended for use by a single thread. 203 * 204 * @author <a href="https://www.revetkn.com">Mark Allen</a> 205 */ 206 @NotThreadSafe 207 public static final class Builder { 208 @NonNull 209 private Integer statusCode; 210 @Nullable 211 private Set<@NonNull ResponseCookie> cookies; 212 @Nullable 213 private Map<@NonNull String, @NonNull Set<@NonNull String>> headers; 214 @Nullable 215 private byte[] body; 216 217 protected Builder(@NonNull Integer statusCode) { 218 requireNonNull(statusCode); 219 this.statusCode = statusCode; 220 } 221 222 @NonNull 223 public Builder statusCode(@NonNull Integer statusCode) { 224 requireNonNull(statusCode); 225 this.statusCode = statusCode; 226 return this; 227 } 228 229 @NonNull 230 public Builder cookies(@Nullable Set<@NonNull ResponseCookie> cookies) { 231 this.cookies = cookies; 232 return this; 233 } 234 235 @NonNull 236 public Builder headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) { 237 this.headers = headers; 238 return this; 239 } 240 241 @NonNull 242 public Builder body(@Nullable byte[] body) { 243 this.body = body; 244 return this; 245 } 246 247 @NonNull 248 public MarshaledResponse build() { 249 return new MarshaledResponse(this); 250 } 251 } 252 253 /** 254 * Builder used to copy instances of {@link MarshaledResponse} via {@link MarshaledResponse#copy()}. 255 * <p> 256 * This class is intended for use by a single thread. 257 * 258 * @author <a href="https://www.revetkn.com">Mark Allen</a> 259 */ 260 @NotThreadSafe 261 public static final class Copier { 262 @NonNull 263 private final Builder builder; 264 265 Copier(@NonNull MarshaledResponse marshaledResponse) { 266 requireNonNull(marshaledResponse); 267 268 this.builder = new Builder(marshaledResponse.getStatusCode()) 269 .headers(new LinkedCaseInsensitiveMap<>(marshaledResponse.getHeaders())) 270 .cookies(new LinkedHashSet<>(marshaledResponse.getCookies())) 271 .body(marshaledResponse.getBody().orElse(null)); 272 } 273 274 @NonNull 275 public Copier statusCode(@NonNull Integer statusCode) { 276 requireNonNull(statusCode); 277 this.builder.statusCode(statusCode); 278 return this; 279 } 280 281 @NonNull 282 public Copier headers(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers) { 283 this.builder.headers(headers); 284 return this; 285 } 286 287 // Convenience method for mutation 288 @NonNull 289 public Copier headers(@NonNull Consumer<Map<@NonNull String, @NonNull Set<@NonNull String>>> headersConsumer) { 290 requireNonNull(headersConsumer); 291 292 if (this.builder.headers == null) 293 this.builder.headers(new LinkedCaseInsensitiveMap<>()); 294 295 headersConsumer.accept(this.builder.headers); 296 return this; 297 } 298 299 @NonNull 300 public Copier cookies(@Nullable Set<@NonNull ResponseCookie> cookies) { 301 this.builder.cookies(cookies); 302 return this; 303 } 304 305 // Convenience method for mutation 306 @NonNull 307 public Copier cookies(@NonNull Consumer<Set<@NonNull ResponseCookie>> cookiesConsumer) { 308 requireNonNull(cookiesConsumer); 309 310 if (this.builder.cookies == null) 311 this.builder.cookies(new LinkedHashSet<>()); 312 313 cookiesConsumer.accept(this.builder.cookies); 314 return this; 315 } 316 317 @NonNull 318 public Copier body(@Nullable byte[] body) { 319 this.builder.body(body); 320 return this; 321 } 322 323 @NonNull 324 public MarshaledResponse finish() { 325 return this.builder.build(); 326 } 327 } 328}