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}