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.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/Web/HTTP/CORS">non-preflight CORS</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 CorsResponse {
044        @NonNull
045        private final String accessControlAllowOrigin;
046        @Nullable
047        private final Boolean accessControlAllowCredentials;
048        @NonNull
049        private final Set<@NonNull String> accessControlExposeHeaders;
050
051        /**
052         * Acquires a builder for {@link CorsResponse} instances.
053         *
054         * @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
055         * @return the builder
056         */
057        @NonNull
058        public static Builder withAccessControlAllowOrigin(@NonNull String accessControlAllowOrigin) {
059                requireNonNull(accessControlAllowOrigin);
060                return new Builder(accessControlAllowOrigin);
061        }
062
063        /**
064         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
065         *
066         * @return a copier for this instance
067         */
068        @NonNull
069        public Copier copy() {
070                return new Copier(this);
071        }
072
073        protected CorsResponse(@NonNull Builder builder) {
074                requireNonNull(builder);
075
076                this.accessControlAllowOrigin = builder.accessControlAllowOrigin;
077                this.accessControlAllowCredentials = builder.accessControlAllowCredentials;
078                this.accessControlExposeHeaders = builder.accessControlExposeHeaders == null ?
079                                Set.of() : Set.copyOf(builder.accessControlExposeHeaders);
080        }
081
082        @Override
083        public String toString() {
084                return format("%s{accessControlAllowOrigin=%s, accessControlAllowCredentials=%s, accessControlExposeHeaders=%s}",
085                                getClass().getSimpleName(), getAccessControlAllowOrigin(), getAccessControlAllowCredentials(),
086                                getAccessControlExposeHeaders());
087        }
088
089        @Override
090        public boolean equals(@Nullable Object object) {
091                if (this == object)
092                        return true;
093
094                if (!(object instanceof CorsResponse corsResponse))
095                        return false;
096
097                return Objects.equals(getAccessControlAllowOrigin(), corsResponse.getAccessControlAllowOrigin())
098                                && Objects.equals(getAccessControlAllowCredentials(), corsResponse.getAccessControlAllowCredentials())
099                                && Objects.equals(getAccessControlExposeHeaders(), corsResponse.getAccessControlExposeHeaders());
100        }
101
102        @Override
103        public int hashCode() {
104                return Objects.hash(getAccessControlAllowOrigin(), getAccessControlAllowCredentials(),
105                                getAccessControlExposeHeaders());
106        }
107
108        /**
109         * 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.
110         *
111         * @return the header value
112         */
113        @NonNull
114        public String getAccessControlAllowOrigin() {
115                return this.accessControlAllowOrigin;
116        }
117
118        /**
119         * 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.
120         *
121         * @return the header value
122         */
123        @NonNull
124        public Optional<Boolean> getAccessControlAllowCredentials() {
125                return Optional.ofNullable(this.accessControlAllowCredentials);
126        }
127
128        /**
129         * Value for the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers">{@code Access-Control-Expose-Headers}</a> response header.
130         *
131         * @return the header value
132         */
133        @NonNull
134        public Set<@NonNull String> getAccessControlExposeHeaders() {
135                return this.accessControlExposeHeaders;
136        }
137
138        /**
139         * Builder used to construct instances of {@link CorsResponse} via {@link CorsResponse#withAccessControlAllowOrigin(String)}.
140         * <p>
141         * This class is intended for use by a single thread.
142         *
143         * @author <a href="https://www.revetkn.com">Mark Allen</a>
144         */
145        @NotThreadSafe
146        public static final class Builder {
147                @NonNull
148                private String accessControlAllowOrigin;
149                @Nullable
150                private Boolean accessControlAllowCredentials;
151                @Nullable
152                private Set<@NonNull String> accessControlExposeHeaders;
153
154                protected Builder(@NonNull String accessControlAllowOrigin) {
155                        requireNonNull(accessControlAllowOrigin);
156                        this.accessControlAllowOrigin = accessControlAllowOrigin;
157                }
158
159                @NonNull
160                public Builder accessControlAllowOrigin(@NonNull String accessControlAllowOrigin) {
161                        requireNonNull(accessControlAllowOrigin);
162                        this.accessControlAllowOrigin = accessControlAllowOrigin;
163                        return this;
164                }
165
166                @NonNull
167                public Builder accessControlAllowCredentials(@Nullable Boolean accessControlAllowCredentials) {
168                        this.accessControlAllowCredentials = accessControlAllowCredentials;
169                        return this;
170                }
171
172                @NonNull
173                public Builder accessControlExposeHeaders(@Nullable Set<@NonNull String> accessControlExposeHeaders) {
174                        this.accessControlExposeHeaders = accessControlExposeHeaders;
175                        return this;
176                }
177
178                @NonNull
179                public CorsResponse build() {
180                        return new CorsResponse(this);
181                }
182        }
183
184        /**
185         * Builder used to copy instances of {@link CorsResponse} via {@link CorsResponse#copy()}.
186         * <p>
187         * This class is intended for use by a single thread.
188         *
189         * @author <a href="https://www.revetkn.com">Mark Allen</a>
190         */
191        @NotThreadSafe
192        public static final class Copier {
193                @NonNull
194                private final Builder builder;
195
196                Copier(@NonNull CorsResponse corsResponse) {
197                        requireNonNull(corsResponse);
198
199                        this.builder = new Builder(corsResponse.getAccessControlAllowOrigin())
200                                        .accessControlAllowCredentials(corsResponse.getAccessControlAllowCredentials().orElse(null))
201                                        .accessControlExposeHeaders(new LinkedHashSet<>(corsResponse.getAccessControlExposeHeaders()));
202                }
203
204                @NonNull
205                public Copier accessControlAllowOrigin(@NonNull String accessControlAllowOrigin) {
206                        requireNonNull(accessControlAllowOrigin);
207                        this.builder.accessControlAllowOrigin(accessControlAllowOrigin);
208                        return this;
209                }
210
211                @NonNull
212                public Copier accessControlAllowCredentials(@Nullable Boolean accessControlAllowCredentials) {
213                        this.builder.accessControlAllowCredentials(accessControlAllowCredentials);
214                        return this;
215                }
216
217                @NonNull
218                public Copier accessControlExposeHeaders(@Nullable Set<@NonNull String> accessControlExposeHeaders) {
219                        this.builder.accessControlExposeHeaders(accessControlExposeHeaders);
220                        return this;
221                }
222
223                // Convenience method for mutation
224                @NonNull
225                public Copier accessControlExposeHeaders(@NonNull Consumer<Set<@NonNull String>> accessControlExposeHeadersConsumer) {
226                        requireNonNull(accessControlExposeHeadersConsumer);
227
228                        if (this.builder.accessControlExposeHeaders == null)
229                                this.builder.accessControlExposeHeaders(new LinkedHashSet<>());
230
231                        accessControlExposeHeadersConsumer.accept(this.builder.accessControlExposeHeaders);
232                        return this;
233                }
234
235                @NonNull
236                public CorsResponse finish() {
237                        return this.builder.build();
238                }
239        }
240}