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