|
10 | 10 | import com.azure.cosmos.implementation.SessionContainer; |
11 | 11 | import com.azure.cosmos.implementation.TestConfigurations; |
12 | 12 | import com.azure.cosmos.implementation.directconnectivity.ReflectionUtils; |
| 13 | +import com.azure.cosmos.models.CosmosQueryRequestOptions; |
| 14 | +import com.azure.cosmos.models.SqlParameter; |
| 15 | +import com.azure.cosmos.models.SqlQuerySpec; |
| 16 | +import com.azure.cosmos.util.CosmosPagedFlux; |
| 17 | +import com.fasterxml.jackson.databind.node.ObjectNode; |
13 | 18 | import org.testng.SkipException; |
14 | 19 | import org.testng.annotations.DataProvider; |
15 | 20 | import org.testng.annotations.Test; |
16 | 21 |
|
17 | 22 | import java.net.URISyntaxException; |
| 23 | +import java.util.ArrayList; |
18 | 24 | import java.util.Arrays; |
| 25 | +import java.util.Comparator; |
| 26 | +import java.util.List; |
| 27 | +import java.util.Objects; |
| 28 | +import java.util.concurrent.ConcurrentHashMap; |
| 29 | +import java.util.concurrent.ConcurrentMap; |
| 30 | +import java.util.stream.Collectors; |
19 | 31 |
|
20 | 32 | import static org.assertj.core.api.Assertions.assertThat; |
| 33 | +import static org.assertj.core.api.Assertions.fail; |
21 | 34 |
|
22 | 35 | public class CosmosClientBuilderTest { |
23 | 36 | String hostName = "https://sample-account.documents.azure.com:443/"; |
@@ -149,4 +162,210 @@ public void validateSessionTokenCapturingForAccountDefaultConsistencyWithEnvVari |
149 | 162 | System.clearProperty("COSMOS.SESSION_CAPTURING_TYPE"); |
150 | 163 | } |
151 | 164 | } |
| 165 | + |
| 166 | + @Test(groups = "emulator") |
| 167 | + public void validateContainerCreationInterceptor() { |
| 168 | + CosmosClient clientWithoutInterceptor = new CosmosClientBuilder() |
| 169 | + .endpoint(TestConfigurations.HOST) |
| 170 | + .key(TestConfigurations.MASTER_KEY) |
| 171 | + .userAgentSuffix("noInterceptor") |
| 172 | + .buildClient(); |
| 173 | + |
| 174 | + ConcurrentMap<CacheKey, List<?>> queryCache = new ConcurrentHashMap<>(); |
| 175 | + |
| 176 | + CosmosClient clientWithInterceptor = new CosmosClientBuilder() |
| 177 | + .endpoint(TestConfigurations.HOST) |
| 178 | + .key(TestConfigurations.MASTER_KEY) |
| 179 | + .userAgentSuffix("withInterceptor") |
| 180 | + .containerCreationInterceptor(originalContainer -> new CacheAndValidateQueriesContainer(originalContainer, queryCache)) |
| 181 | + .buildClient(); |
| 182 | + |
| 183 | + CosmosAsyncClient asyncClientWithInterceptor = new CosmosClientBuilder() |
| 184 | + .endpoint(TestConfigurations.HOST) |
| 185 | + .key(TestConfigurations.MASTER_KEY) |
| 186 | + .userAgentSuffix("withInterceptor") |
| 187 | + .containerCreationInterceptor(originalContainer -> new CacheAndValidateQueriesContainer(originalContainer, queryCache)) |
| 188 | + .buildAsyncClient(); |
| 189 | + |
| 190 | + CosmosContainer normalContainer = clientWithoutInterceptor |
| 191 | + .getDatabase("TestDB") |
| 192 | + .getContainer("TestContainer"); |
| 193 | + assertThat(normalContainer).isNotNull(); |
| 194 | + assertThat(normalContainer.getClass()).isEqualTo(CosmosContainer.class); |
| 195 | + assertThat(normalContainer.asyncContainer.getClass()).isEqualTo(CosmosAsyncContainer.class); |
| 196 | + |
| 197 | + CosmosContainer customSyncContainer = clientWithInterceptor |
| 198 | + .getDatabase("TestDB") |
| 199 | + .getContainer("TestContainer"); |
| 200 | + assertThat(customSyncContainer).isNotNull(); |
| 201 | + assertThat(customSyncContainer.getClass()).isEqualTo(CosmosContainer.class); |
| 202 | + assertThat(customSyncContainer.asyncContainer.getClass()).isEqualTo(CacheAndValidateQueriesContainer.class); |
| 203 | + |
| 204 | + CosmosAsyncContainer customAsyncContainer = asyncClientWithInterceptor |
| 205 | + .getDatabase("TestDB") |
| 206 | + .getContainer("TestContainer"); |
| 207 | + assertThat(customAsyncContainer).isNotNull(); |
| 208 | + assertThat(customAsyncContainer.getClass()).isEqualTo(CacheAndValidateQueriesContainer.class); |
| 209 | + |
| 210 | + try { |
| 211 | + customSyncContainer.queryItems("SELECT * from c", null, ObjectNode.class); |
| 212 | + fail("Unparameterized query should throw"); |
| 213 | + } catch (IllegalStateException expectedError) {} |
| 214 | + |
| 215 | + try { |
| 216 | + customAsyncContainer.queryItems("SELECT * from c", null, ObjectNode.class); |
| 217 | + fail("Unparameterized query should throw"); |
| 218 | + } catch (IllegalStateException expectedError) {} |
| 219 | + |
| 220 | + try { |
| 221 | + customAsyncContainer.queryItems("SELECT * from c", ObjectNode.class); |
| 222 | + fail("Unparameterized query should throw"); |
| 223 | + } catch (IllegalStateException expectedError) {} |
| 224 | + |
| 225 | + SqlQuerySpec querySpec = new SqlQuerySpec().setQueryText("SELECT * from c"); |
| 226 | + assertThat(queryCache).size().isEqualTo(0); |
| 227 | + |
| 228 | + try { |
| 229 | + List<ObjectNode> items = customSyncContainer |
| 230 | + .queryItems(querySpec, null, ObjectNode.class) |
| 231 | + .stream().collect(Collectors.toList()); |
| 232 | + fail("Not yet cached - the query above should always throw"); |
| 233 | + } catch (CosmosException cosmosException) { |
| 234 | + // Container does not exist - when not cached should fail |
| 235 | + assertThat(cosmosException.getStatusCode()).isEqualTo(404); |
| 236 | + assertThat(cosmosException.getSubStatusCode()).isEqualTo(1003); |
| 237 | + } |
| 238 | + |
| 239 | + queryCache.putIfAbsent(new CacheKey(ObjectNode.class.getCanonicalName(), querySpec), new ArrayList<>()); |
| 240 | + assertThat(queryCache).size().isEqualTo(1); |
| 241 | + |
| 242 | + // Validate that CacheKey equality check works |
| 243 | + queryCache.putIfAbsent(new CacheKey(ObjectNode.class.getCanonicalName(), querySpec), new ArrayList<>()); |
| 244 | + assertThat(queryCache).size().isEqualTo(1); |
| 245 | + |
| 246 | + // Validate that form cache the results can be served |
| 247 | + List<ObjectNode> items = customSyncContainer |
| 248 | + .queryItems(querySpec, null, ObjectNode.class) |
| 249 | + .stream().collect(Collectors.toList()); |
| 250 | + |
| 251 | + querySpec = new SqlQuerySpec().setQueryText("SELECT * from c"); |
| 252 | + CosmosPagedFlux<ObjectNode> cachedPagedFlux = customAsyncContainer |
| 253 | + .queryItems(querySpec, null, ObjectNode.class); |
| 254 | + assertThat(cachedPagedFlux.getClass().getName()).startsWith("com.azure.cosmos.util.CosmosPagedFluxStaticListImpl"); |
| 255 | + |
| 256 | + // Validate that uncached query form async Container also fails with 404 due to non-existing Container |
| 257 | + querySpec = new SqlQuerySpec().setQueryText("SELECT * from r"); |
| 258 | + try { |
| 259 | + CosmosPagedFlux<ObjectNode> uncachedPagedFlux = customAsyncContainer |
| 260 | + .queryItems(querySpec, null, ObjectNode.class); |
| 261 | + } catch (CosmosException cosmosException) { |
| 262 | + assertThat(cosmosException.getStatusCode()).isEqualTo(404); |
| 263 | + assertThat(cosmosException.getSubStatusCode()).isEqualTo(1003); |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + private static class CacheKey { |
| 268 | + private final String className; |
| 269 | + private final String queryText; |
| 270 | + |
| 271 | + private final List<SqlParameter> parameters; |
| 272 | + public CacheKey(String className, SqlQuerySpec querySpec) { |
| 273 | + this.className = className; |
| 274 | + this.queryText = querySpec.getQueryText(); |
| 275 | + List<SqlParameter> tempParameters = querySpec.getParameters(); |
| 276 | + if (tempParameters != null) { |
| 277 | + tempParameters.sort(Comparator.comparing(SqlParameter::getName)); |
| 278 | + this.parameters = tempParameters; |
| 279 | + } else { |
| 280 | + this.parameters = new ArrayList<>(); |
| 281 | + } |
| 282 | + } |
| 283 | + |
| 284 | + @Override |
| 285 | + public int hashCode() { |
| 286 | + Object[] temp = new Object[2 + this.parameters.size()]; |
| 287 | + temp[0] = this.className; |
| 288 | + temp[1] = this.queryText; |
| 289 | + for (int i = 0; i < this.parameters.size(); i++) { |
| 290 | + temp[2 + i] = this.parameters.get(i).getValue(Object.class); |
| 291 | + } |
| 292 | + |
| 293 | + return Objects.hash(temp); |
| 294 | + } |
| 295 | + |
| 296 | + @Override |
| 297 | + public boolean equals(Object obj) { |
| 298 | + if (obj == null) { |
| 299 | + return false; |
| 300 | + } |
| 301 | + |
| 302 | + if (!(obj instanceof CacheKey)) { |
| 303 | + return false; |
| 304 | + } |
| 305 | + |
| 306 | + CacheKey other = (CacheKey)obj; |
| 307 | + if (!this.className.equals(other.className)) { |
| 308 | + return false; |
| 309 | + } |
| 310 | + |
| 311 | + if (!this.queryText.equals(other.queryText)) { |
| 312 | + return false; |
| 313 | + } |
| 314 | + |
| 315 | + if (this.parameters.size() != other.parameters.size()) { |
| 316 | + return false; |
| 317 | + } |
| 318 | + |
| 319 | + for (int i = 0; i < this.parameters.size(); i++) { |
| 320 | + if (!this.parameters.get(i).getName().equals(other.parameters.get(i).getName())) { |
| 321 | + return false; |
| 322 | + } |
| 323 | + |
| 324 | + if (!this.parameters.get(i).getValue(Object.class).equals(other.parameters.get(i).getValue(Object.class))) { |
| 325 | + return false; |
| 326 | + } |
| 327 | + } |
| 328 | + |
| 329 | + return true; |
| 330 | + } |
| 331 | + } |
| 332 | + |
| 333 | + private static class CacheAndValidateQueriesContainer extends CosmosAsyncContainer { |
| 334 | + private final ConcurrentMap<CacheKey, List<?>> queryCache; |
| 335 | + |
| 336 | + protected CacheAndValidateQueriesContainer( |
| 337 | + CosmosAsyncContainer toBeWrappedContainer, |
| 338 | + ConcurrentMap<CacheKey, List<?>> queryCache) { |
| 339 | + |
| 340 | + super(toBeWrappedContainer); |
| 341 | + this.queryCache = queryCache; |
| 342 | + } |
| 343 | + |
| 344 | + @Override |
| 345 | + public <T> CosmosPagedFlux<T> queryItems(String query, CosmosQueryRequestOptions options, Class<T> classType) { |
| 346 | + throw new IllegalStateException("No unparameterized queries allowed. Use parameterized query instead."); |
| 347 | + } |
| 348 | + |
| 349 | + @Override |
| 350 | + public <T> CosmosPagedFlux<T> queryItems(SqlQuerySpec querySpec, Class<T> classType) { |
| 351 | + return this.queryItems(querySpec, null, classType); |
| 352 | + } |
| 353 | + |
| 354 | + @Override |
| 355 | + public <T> CosmosPagedFlux<T> queryItems(String query, Class<T> classType) { |
| 356 | + throw new IllegalStateException("No unparameterized queries allowed. Use parameterized query instead."); |
| 357 | + } |
| 358 | + |
| 359 | + @Override |
| 360 | + public <T> CosmosPagedFlux<T> queryItems(SqlQuerySpec querySpec, CosmosQueryRequestOptions options, Class<T> classType) { |
| 361 | + CacheKey key = new CacheKey(classType.getCanonicalName(), querySpec); |
| 362 | + List<?> cachedResult = this.queryCache.get(key); |
| 363 | + if (cachedResult != null) { |
| 364 | + return CosmosPagedFlux.createFromList((List<T>)cachedResult, false); |
| 365 | + } |
| 366 | + |
| 367 | + return super |
| 368 | + .queryItems(querySpec, options, classType); |
| 369 | + } |
| 370 | + } |
152 | 371 | } |
0 commit comments