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