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}