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}