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}