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