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}