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; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.time.Duration; 025import java.util.LinkedHashSet; 026import java.util.Objects; 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 * Response headers to send over the wire for <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a> requests. 036 * <p> 037 * Instances can be acquired via the {@link #withAccessControlAllowOrigin(String)} builder factory method. 038 * <p> 039 * See <a href="https://www.soklet.com/docs/cors#writing-cors-responses">https://www.soklet.com/docs/cors#writing-cors-responses</a> for detailed documentation. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class CorsPreflightResponse { 045 @NonNull 046 private final String accessControlAllowOrigin; 047 @Nullable 048 private final Boolean accessControlAllowCredentials; 049 @Nullable 050 private final Duration accessControlMaxAge; 051 @NonNull 052 private final Set<@NonNull HttpMethod> accessControlAllowMethods; 053 @NonNull 054 private final Set<@NonNull String> accessControlAllowHeaders; 055 056 /** 057 * Acquires a builder for {@link CorsPreflightResponse} instances. 058 * 059 * @param accessControlAllowOrigin the required <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">{@code Access-Control-Allow-Origin}</a> response header value 060 * @return the builder 061 */ 062 @NonNull 063 public static Builder withAccessControlAllowOrigin(@NonNull String accessControlAllowOrigin) { 064 requireNonNull(accessControlAllowOrigin); 065 return new Builder(accessControlAllowOrigin); 066 } 067 068 /** 069 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 070 * 071 * @return a copier for this instance 072 */ 073 @NonNull 074 public Copier copy() { 075 return new Copier(this); 076 } 077 078 protected CorsPreflightResponse(@NonNull Builder builder) { 079 requireNonNull(builder); 080 081 this.accessControlAllowOrigin = builder.accessControlAllowOrigin; 082 this.accessControlAllowCredentials = builder.accessControlAllowCredentials; 083 this.accessControlMaxAge = builder.accessControlMaxAge; 084 this.accessControlAllowMethods = builder.accessControlAllowMethods == null ? 085 Set.of() : Set.copyOf(builder.accessControlAllowMethods); 086 this.accessControlAllowHeaders = builder.accessControlAllowHeaders == null ? 087 Set.of() : Set.copyOf(builder.accessControlAllowHeaders); 088 } 089 090 @Override 091 public String toString() { 092 return format("%s{accessControlAllowOrigin=%s, accessControlAllowCredentials=%s, " + 093 "accessControlMaxAge=%s, accessControlAllowMethods=%s, accessControlAllowHeaders=%s}", 094 getClass().getSimpleName(), getAccessControlAllowOrigin(), getAccessControlAllowCredentials(), 095 getAccessControlMaxAge(), getAccessControlAllowMethods(), getAccessControlAllowHeaders()); 096 } 097 098 @Override 099 public boolean equals(@Nullable Object object) { 100 if (this == object) 101 return true; 102 103 if (!(object instanceof CorsPreflightResponse corsPreflightResponse)) 104 return false; 105 106 return Objects.equals(getAccessControlAllowOrigin(), corsPreflightResponse.getAccessControlAllowOrigin()) 107 && Objects.equals(getAccessControlAllowCredentials(), corsPreflightResponse.getAccessControlAllowCredentials()) 108 && Objects.equals(getAccessControlMaxAge(), corsPreflightResponse.getAccessControlMaxAge()) 109 && Objects.equals(getAccessControlAllowMethods(), corsPreflightResponse.getAccessControlAllowMethods()) 110 && Objects.equals(getAccessControlAllowHeaders(), corsPreflightResponse.getAccessControlAllowHeaders()); 111 } 112 113 @Override 114 public int hashCode() { 115 return Objects.hash(getAccessControlAllowOrigin(), getAccessControlAllowCredentials(), 116 getAccessControlMaxAge(), getAccessControlAllowMethods(), getAccessControlAllowHeaders()); 117 } 118 119 /** 120 * Value for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">{@code Access-Control-Allow-Origin}</a> response header. 121 * 122 * @return the header value 123 */ 124 @NonNull 125 public String getAccessControlAllowOrigin() { 126 return this.accessControlAllowOrigin; 127 } 128 129 /** 130 * Value for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials">{@code Access-Control-Allow-Credentials}</a> response header. 131 * 132 * @return the header value, or {@link Optional#empty()} if not specified 133 */ 134 @NonNull 135 public Optional<Boolean> getAccessControlAllowCredentials() { 136 return Optional.ofNullable(this.accessControlAllowCredentials); 137 } 138 139 /** 140 * Value for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age">{@code Access-Control-Max-Age}</a> response header. 141 * 142 * @return the header value, or {@link Optional#empty()} if not specified 143 */ 144 @NonNull 145 public Optional<Duration> getAccessControlMaxAge() { 146 return Optional.ofNullable(this.accessControlMaxAge); 147 } 148 149 /** 150 * Set of values for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods">{@code Access-Control-Allow-Methods}</a> response header. 151 * 152 * @return the header values, or the empty set if not specified 153 */ 154 @NonNull 155 public Set<@NonNull HttpMethod> getAccessControlAllowMethods() { 156 return this.accessControlAllowMethods; 157 } 158 159 /** 160 * Set of values for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers">{@code Access-Control-Allow-Headers}</a> response header. 161 * 162 * @return the header values, or the empty set if not specified 163 */ 164 @NonNull 165 public Set<@NonNull String> getAccessControlAllowHeaders() { 166 return this.accessControlAllowHeaders; 167 } 168 169 /** 170 * Builder used to construct instances of {@link CorsPreflightResponse} via {@link CorsPreflightResponse#withAccessControlAllowOrigin(String)}. 171 * <p> 172 * This class is intended for use by a single thread. 173 * 174 * @author <a href="https://www.revetkn.com">Mark Allen</a> 175 */ 176 @NotThreadSafe 177 public static final class Builder { 178 @NonNull 179 private String accessControlAllowOrigin; 180 @Nullable 181 private Boolean accessControlAllowCredentials; 182 @Nullable 183 private Duration accessControlMaxAge; 184 @Nullable 185 private Set<@NonNull HttpMethod> accessControlAllowMethods; 186 @Nullable 187 private Set<@NonNull String> accessControlAllowHeaders; 188 189 protected Builder(@NonNull String accessControlAllowOrigin) { 190 requireNonNull(accessControlAllowOrigin); 191 this.accessControlAllowOrigin = accessControlAllowOrigin; 192 } 193 194 @NonNull 195 public Builder accessControlAllowOrigin(@NonNull String accessControlAllowOrigin) { 196 requireNonNull(accessControlAllowOrigin); 197 this.accessControlAllowOrigin = accessControlAllowOrigin; 198 return this; 199 } 200 201 @NonNull 202 public Builder accessControlAllowCredentials(@Nullable Boolean accessControlAllowCredentials) { 203 this.accessControlAllowCredentials = accessControlAllowCredentials; 204 return this; 205 } 206 207 @NonNull 208 public Builder accessControlMaxAge(@Nullable Duration accessControlMaxAge) { 209 this.accessControlMaxAge = accessControlMaxAge; 210 return this; 211 } 212 213 @NonNull 214 public Builder accessControlAllowMethods(@Nullable Set<@NonNull HttpMethod> accessControlAllowMethods) { 215 this.accessControlAllowMethods = accessControlAllowMethods; 216 return this; 217 } 218 219 @NonNull 220 public Builder accessControlAllowHeaders(@Nullable Set<@NonNull String> accessControlAllowHeaders) { 221 this.accessControlAllowHeaders = accessControlAllowHeaders; 222 return this; 223 } 224 225 @NonNull 226 public CorsPreflightResponse build() { 227 return new CorsPreflightResponse(this); 228 } 229 } 230 231 /** 232 * Builder used to copy instances of {@link CorsPreflightResponse} via {@link CorsPreflightResponse#copy()}. 233 * <p> 234 * This class is intended for use by a single thread. 235 * 236 * @author <a href="https://www.revetkn.com">Mark Allen</a> 237 */ 238 @NotThreadSafe 239 public static final class Copier { 240 @NonNull 241 private final Builder builder; 242 243 Copier(@NonNull CorsPreflightResponse corsPreflightResponse) { 244 requireNonNull(corsPreflightResponse); 245 246 this.builder = new Builder(corsPreflightResponse.getAccessControlAllowOrigin()) 247 .accessControlAllowCredentials(corsPreflightResponse.getAccessControlAllowCredentials().orElse(null)) 248 .accessControlMaxAge(corsPreflightResponse.getAccessControlMaxAge().orElse(null)) 249 .accessControlAllowMethods(new LinkedHashSet<>(corsPreflightResponse.getAccessControlAllowMethods())) 250 .accessControlAllowHeaders(new LinkedHashSet<>(corsPreflightResponse.getAccessControlAllowHeaders())); 251 } 252 253 @NonNull 254 public Copier accessControlAllowOrigin(@NonNull String accessControlAllowOrigin) { 255 requireNonNull(accessControlAllowOrigin); 256 this.builder.accessControlAllowOrigin(accessControlAllowOrigin); 257 return this; 258 } 259 260 @NonNull 261 public Copier accessControlAllowCredentials(@Nullable Boolean accessControlAllowCredentials) { 262 this.builder.accessControlAllowCredentials(accessControlAllowCredentials); 263 return this; 264 } 265 266 @NonNull 267 public Copier accessControlMaxAge(@Nullable Duration accessControlMaxAge) { 268 this.builder.accessControlMaxAge(accessControlMaxAge); 269 return this; 270 } 271 272 @NonNull 273 public Copier accessControlAllowMethods(@Nullable Set<@NonNull HttpMethod> accessControlAllowMethods) { 274 this.builder.accessControlAllowMethods(accessControlAllowMethods); 275 return this; 276 } 277 278 // Convenience method for mutation 279 @NonNull 280 public Copier accessControlAllowMethods(@NonNull Consumer<Set<@NonNull HttpMethod>> accessControlAllowMethodsConsumer) { 281 requireNonNull(accessControlAllowMethodsConsumer); 282 283 if (this.builder.accessControlAllowMethods == null) 284 this.builder.accessControlAllowMethods(new LinkedHashSet<>()); 285 286 accessControlAllowMethodsConsumer.accept(this.builder.accessControlAllowMethods); 287 return this; 288 } 289 290 @NonNull 291 public Copier accessControlAllowHeaders(@Nullable Set<@NonNull String> accessControlAllowHeaders) { 292 this.builder.accessControlAllowHeaders(accessControlAllowHeaders); 293 return this; 294 } 295 296 // Convenience method for mutation 297 @NonNull 298 public Copier accessControlAllowHeaders(@NonNull Consumer<Set<@NonNull String>> accessControlAllowHeadersConsumer) { 299 requireNonNull(accessControlAllowHeadersConsumer); 300 301 if (this.builder.accessControlAllowHeaders == null) 302 this.builder.accessControlAllowHeaders(new LinkedHashSet<>()); 303 304 accessControlAllowHeadersConsumer.accept(this.builder.accessControlAllowHeaders); 305 return this; 306 } 307 308 @NonNull 309 public CorsPreflightResponse finish() { 310 return this.builder.build(); 311 } 312 } 313}