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.ArrayList; 024import java.util.List; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.stream.Collectors; 028 029import static java.lang.String.format; 030import static java.util.Objects.requireNonNull; 031 032/** 033 * Encapsulates the results of a request that would normally be handled by your {@link Server} (both logical response and bytes to be sent over the wire), used for integration testing via {@link Simulator#performRequest(Request)}. 034 * <p> 035 * Instances can be acquired via the {@link #withMarshaledResponse(MarshaledResponse)} factory method. 036 * <p> 037 * The Server-Sent Event equivalent of this type is {@link ServerSentEventRequestResult}, which is used for integration testing via {@link Simulator#performServerSentEventRequest(Request)}. 038 * <p> 039 * See <a href="https://www.soklet.com/docs/testing#integration-testing">https://www.soklet.com/docs/testing#integration-testing</a> for detailed documentation. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class RequestResult { 045 @Nonnull 046 private final MarshaledResponse marshaledResponse; 047 @Nullable 048 private final Response response; 049 @Nullable 050 private final CorsPreflightResponse corsPreflightResponse; 051 @Nullable 052 private final ResourceMethod resourceMethod; 053 @Nullable 054 private final HandshakeResult handshakeResult; 055 056 /** 057 * Acquires a builder for {@link RequestResult} instances. 058 * 059 * @param marshaledResponse the bytes that will ultimately be written over the wire 060 * @return the builder 061 */ 062 @Nonnull 063 public static Builder withMarshaledResponse(@Nonnull MarshaledResponse marshaledResponse) { 064 requireNonNull(marshaledResponse); 065 return new Builder(marshaledResponse); 066 } 067 068 /** 069 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 070 * 071 * @return a copier for this instance 072 */ 073 @Nonnull 074 public Copier copy() { 075 return new Copier(this); 076 } 077 078 protected RequestResult(@Nonnull Builder builder) { 079 requireNonNull(builder); 080 081 this.marshaledResponse = builder.marshaledResponse; 082 this.response = builder.response; 083 this.corsPreflightResponse = builder.corsPreflightResponse; 084 this.resourceMethod = builder.resourceMethod; 085 this.handshakeResult = builder.handshakeResult; 086 } 087 088 @Override 089 public String toString() { 090 List<String> components = new ArrayList<>(5); 091 092 components.add(format("marshaledResponse=%s", getMarshaledResponse())); 093 094 Response response = getResponse().orElse(null); 095 096 if (response != null) 097 components.add(format("response=%s", response)); 098 099 CorsPreflightResponse corsPreflightResponse = getCorsPreflightResponse().orElse(null); 100 101 if (corsPreflightResponse != null) 102 components.add(format("corsPreflightResponse=%s", corsPreflightResponse)); 103 104 ResourceMethod resourceMethod = getResourceMethod().orElse(null); 105 106 if (resourceMethod != null) 107 components.add(format("resourceMethod=%s", resourceMethod)); 108 109 // Hide this for now because handshake info is package-private and we don't want it to leak out 110 111 // HandshakeResult handshakeResult = getHandshakeResult().orElse(null); 112 113 // if (handshakeResult != null) 114 // components.add(format("handshakeResult=%s", handshakeResult)); 115 116 return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", "))); 117 } 118 119 @Override 120 public boolean equals(@Nullable Object object) { 121 if (this == object) 122 return true; 123 124 if (!(object instanceof RequestResult requestResult)) 125 return false; 126 127 return Objects.equals(getMarshaledResponse(), requestResult.getMarshaledResponse()) 128 && Objects.equals(getResponse(), requestResult.getResponse()) 129 && Objects.equals(getCorsPreflightResponse(), requestResult.getCorsPreflightResponse()) 130 && Objects.equals(getResourceMethod(), requestResult.getResourceMethod()) 131 && Objects.equals(getHandshakeResult(), requestResult.getHandshakeResult()); 132 } 133 134 @Override 135 public int hashCode() { 136 return Objects.hash(getMarshaledResponse(), getResponse(), getCorsPreflightResponse(), getResourceMethod(), getHandshakeResult()); 137 } 138 139 /** 140 * The final representation of the response to be written over the wire. 141 * 142 * @return the response to be written over the wire 143 */ 144 @Nonnull 145 public MarshaledResponse getMarshaledResponse() { 146 return this.marshaledResponse; 147 } 148 149 /** 150 * The logical response, determined by the return value of the <em>Resource Method</em> (if available). 151 * 152 * @return the logical response 153 */ 154 @Nonnull 155 public Optional<Response> getResponse() { 156 return Optional.ofNullable(this.response); 157 } 158 159 /** 160 * The CORS preflight logical response, if applicable for the request. 161 * 162 * @return the CORS preflight logical response 163 */ 164 @Nonnull 165 public Optional<CorsPreflightResponse> getCorsPreflightResponse() { 166 return Optional.ofNullable(this.corsPreflightResponse); 167 } 168 169 /** 170 * The <em>Resource Method</em> that handled the request, if available. 171 * 172 * @return the <em>Resource Method</em> that handled the request 173 */ 174 @Nonnull 175 public Optional<ResourceMethod> getResourceMethod() { 176 return Optional.ofNullable(this.resourceMethod); 177 } 178 179 180 /** 181 * The SSE handshake result, if available. 182 * 183 * @return the SSE handshake result 184 */ 185 @Nonnull 186 Optional<HandshakeResult> getHandshakeResult() { 187 return Optional.ofNullable(this.handshakeResult); 188 } 189 190 /** 191 * Builder used to construct instances of {@link RequestResult} via {@link RequestResult#withMarshaledResponse(MarshaledResponse)}. 192 * <p> 193 * This class is intended for use by a single thread. 194 * 195 * @author <a href="https://www.revetkn.com">Mark Allen</a> 196 */ 197 @NotThreadSafe 198 public static final class Builder { 199 @Nonnull 200 private MarshaledResponse marshaledResponse; 201 @Nullable 202 private Response response; 203 @Nullable 204 private CorsPreflightResponse corsPreflightResponse; 205 @Nullable 206 private ResourceMethod resourceMethod; 207 @Nullable 208 private HandshakeResult handshakeResult; 209 210 protected Builder(@Nonnull MarshaledResponse marshaledResponse) { 211 requireNonNull(marshaledResponse); 212 this.marshaledResponse = marshaledResponse; 213 } 214 215 @Nonnull 216 public Builder marshaledResponse(@Nonnull MarshaledResponse marshaledResponse) { 217 requireNonNull(marshaledResponse); 218 this.marshaledResponse = marshaledResponse; 219 return this; 220 } 221 222 @Nonnull 223 public Builder response(@Nullable Response response) { 224 this.response = response; 225 return this; 226 } 227 228 @Nonnull 229 public Builder corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) { 230 this.corsPreflightResponse = corsPreflightResponse; 231 return this; 232 } 233 234 @Nonnull 235 public Builder resourceMethod(@Nullable ResourceMethod resourceMethod) { 236 this.resourceMethod = resourceMethod; 237 return this; 238 } 239 240 @Nonnull 241 Builder handshakeResult(@Nullable HandshakeResult handshakeResult) { 242 this.handshakeResult = handshakeResult; 243 return this; 244 } 245 246 @Nonnull 247 public RequestResult build() { 248 return new RequestResult(this); 249 } 250 } 251 252 /** 253 * Builder used to copy instances of {@link RequestResult} via {@link RequestResult#copy()}. 254 * <p> 255 * This class is intended for use by a single thread. 256 * 257 * @author <a href="https://www.revetkn.com">Mark Allen</a> 258 */ 259 @NotThreadSafe 260 public static final class Copier { 261 @Nonnull 262 private final Builder builder; 263 264 Copier(@Nonnull RequestResult requestResult) { 265 requireNonNull(requestResult); 266 267 this.builder = new Builder(requestResult.getMarshaledResponse()) 268 .response(requestResult.getResponse().orElse(null)) 269 .corsPreflightResponse(requestResult.getCorsPreflightResponse().orElse(null)) 270 .resourceMethod(requestResult.getResourceMethod().orElse(null)) 271 .handshakeResult(requestResult.getHandshakeResult().orElse(null)); 272 } 273 274 @Nonnull 275 public Copier marshaledResponse(@Nonnull MarshaledResponse marshaledResponse) { 276 requireNonNull(marshaledResponse); 277 this.builder.marshaledResponse(marshaledResponse); 278 return this; 279 } 280 281 @Nonnull 282 public Copier response(@Nullable Response response) { 283 this.builder.response(response); 284 return this; 285 } 286 287 @Nonnull 288 public Copier corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) { 289 this.builder.corsPreflightResponse(corsPreflightResponse); 290 return this; 291 } 292 293 @Nonnull 294 public Copier resourceMethod(@Nullable ResourceMethod resourceMethod) { 295 this.builder.resourceMethod(resourceMethod); 296 return this; 297 } 298 299 @Nonnull 300 Copier handshakeResult(@Nullable HandshakeResult handshakeResult) { 301 this.builder.handshakeResult(handshakeResult); 302 return this; 303 } 304 305 @Nonnull 306 public RequestResult finish() { 307 return this.builder.build(); 308 } 309 } 310}