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.LinkedHashSet; 026import java.util.Map; 027import java.util.Optional; 028import java.util.Set; 029import java.util.function.Consumer; 030 031import static java.lang.String.format; 032import static java.util.Objects.requireNonNull; 033 034/** 035 * A finalized representation of a {@link Response}, suitable for sending to clients over the wire. 036 * <p> 037 * Your application's {@link ResponseMarshaler} is responsible for taking the {@link Response} returned by a <em>Resource Method</em> as input 038 * and converting its {@link Response#getBody()} to a {@code byte[]}. 039 * <p> 040 * For example, if a {@link Response} were to specify a body of {@code List.of("one", "two")}, a {@link ResponseMarshaler} might 041 * 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"]}. 042 * <p> 043 * Full documentation is available at <a href="https://www.soklet.com/docs/response-writing">https://www.soklet.com/docs/response-writing</a>. 044 * 045 * @author <a href="https://www.revetkn.com">Mark Allen</a> 046 */ 047@ThreadSafe 048public class MarshaledResponse { 049 @Nonnull 050 private final Integer statusCode; 051 @Nonnull 052 private final Map<String, Set<String>> headers; 053 @Nonnull 054 private final Set<ResponseCookie> cookies; 055 @Nullable 056 private final byte[] body; 057 058 /** 059 * Acquires a builder for {@link MarshaledResponse} instances. 060 * 061 * @param statusCode the HTTP status code for this response 062 * @return the builder 063 */ 064 @Nonnull 065 public static Builder withStatusCode(@Nonnull Integer statusCode) { 066 requireNonNull(statusCode); 067 return new Builder(statusCode); 068 } 069 070 /** 071 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 072 * 073 * @return a copier for this instance 074 */ 075 @Nonnull 076 public Copier copy() { 077 return new Copier(this); 078 } 079 080 protected MarshaledResponse(@Nonnull Builder builder) { 081 requireNonNull(builder); 082 083 this.statusCode = builder.statusCode; 084 this.headers = builder.headers == null ? Map.of() : new LinkedCaseInsensitiveMap<>(builder.headers); 085 this.cookies = builder.cookies == null ? Set.of() : new LinkedHashSet<>(builder.cookies); 086 this.body = builder.body; 087 } 088 089 @Override 090 public String toString() { 091 return format("%s{statusCode=%s, headers=%s, cookies=%s, body=%s}", getClass().getSimpleName(), 092 getStatusCode(), getHeaders(), getCookies(), 093 format("%d bytes", getBody().isPresent() ? getBody().get().length : 0)); 094 } 095 096 /** 097 * The HTTP status code for this response. 098 * 099 * @return the status code 100 */ 101 @Nonnull 102 public Integer getStatusCode() { 103 return this.statusCode; 104 } 105 106 /** 107 * The HTTP headers to write for this response. 108 * 109 * @return the headers to write 110 */ 111 @Nonnull 112 public Map<String, Set<String>> getHeaders() { 113 return this.headers; 114 } 115 116 /** 117 * The HTTP cookies to write for this response. 118 * 119 * @return the cookies to write 120 */ 121 @Nonnull 122 public Set<ResponseCookie> getCookies() { 123 return this.cookies; 124 } 125 126 /** 127 * The HTTP response body to write, if available. 128 * 129 * @return the response body to write, or {@link Optional#empty()}) if no body should be written 130 */ 131 @Nonnull 132 public Optional<byte[]> getBody() { 133 return Optional.ofNullable(this.body); 134 } 135 136 /** 137 * Builder used to construct instances of {@link MarshaledResponse} via {@link MarshaledResponse#withStatusCode(Integer)}. 138 * <p> 139 * This class is intended for use by a single thread. 140 * 141 * @author <a href="https://www.revetkn.com">Mark Allen</a> 142 */ 143 @NotThreadSafe 144 public static class Builder { 145 @Nonnull 146 private Integer statusCode; 147 @Nullable 148 private Set<ResponseCookie> cookies; 149 @Nullable 150 private Map<String, Set<String>> headers; 151 @Nullable 152 private byte[] body; 153 154 protected Builder(@Nonnull Integer statusCode) { 155 requireNonNull(statusCode); 156 this.statusCode = statusCode; 157 } 158 159 @Nonnull 160 public Builder statusCode(@Nonnull Integer statusCode) { 161 requireNonNull(statusCode); 162 this.statusCode = statusCode; 163 return this; 164 } 165 166 @Nonnull 167 public Builder cookies(@Nullable Set<ResponseCookie> cookies) { 168 this.cookies = cookies; 169 return this; 170 } 171 172 @Nonnull 173 public Builder headers(@Nullable Map<String, Set<String>> headers) { 174 this.headers = headers; 175 return this; 176 } 177 178 @Nonnull 179 public Builder body(@Nullable byte[] body) { 180 this.body = body; 181 return this; 182 } 183 184 @Nonnull 185 public MarshaledResponse build() { 186 return new MarshaledResponse(this); 187 } 188 } 189 190 /** 191 * Builder used to copy instances of {@link MarshaledResponse} via {@link MarshaledResponse#copy()}. 192 * <p> 193 * This class is intended for use by a single thread. 194 * 195 * @author <a href="https://www.revetkn.com">Mark Allen</a> 196 */ 197 @NotThreadSafe 198 public static class Copier { 199 @Nonnull 200 private final Builder builder; 201 202 Copier(@Nonnull MarshaledResponse marshaledResponse) { 203 requireNonNull(marshaledResponse); 204 205 this.builder = new Builder(marshaledResponse.getStatusCode()) 206 .headers(new LinkedCaseInsensitiveMap<>(marshaledResponse.getHeaders())) 207 .cookies(new LinkedHashSet<>(marshaledResponse.getCookies())) 208 .body(marshaledResponse.getBody().orElse(null)); 209 } 210 211 @Nonnull 212 public Copier statusCode(@Nonnull Integer statusCode) { 213 requireNonNull(statusCode); 214 this.builder.statusCode(statusCode); 215 return this; 216 } 217 218 @Nonnull 219 public Copier headers(@Nonnull Map<String, Set<String>> headers) { 220 this.builder.headers(headers); 221 return this; 222 } 223 224 // Convenience method for mutation 225 @Nonnull 226 public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) { 227 requireNonNull(headersConsumer); 228 229 if (this.builder.headers == null) 230 this.builder.headers(new LinkedCaseInsensitiveMap<>()); 231 232 headersConsumer.accept(this.builder.headers); 233 return this; 234 } 235 236 @Nonnull 237 public Copier cookies(@Nullable Set<ResponseCookie> cookies) { 238 this.builder.cookies(cookies); 239 return this; 240 } 241 242 // Convenience method for mutation 243 @Nonnull 244 public Copier cookies(@Nonnull Consumer<Set<ResponseCookie>> cookiesConsumer) { 245 requireNonNull(cookiesConsumer); 246 247 if (this.builder.cookies == null) 248 this.builder.cookies(new LinkedHashSet<>()); 249 250 cookiesConsumer.accept(this.builder.cookies); 251 return this; 252 } 253 254 @Nonnull 255 public Copier body(@Nullable byte[] body) { 256 this.builder.body(body); 257 return this; 258 } 259 260 @Nonnull 261 public MarshaledResponse finish() { 262 return this.builder.build(); 263 } 264 } 265}