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}