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