Skip to content

Commit 5ec8d96

Browse files
authored
Fixed issue with single quote characters in partition keys and row keys in Azure Data Tables (Azure#25066)
* Created base classes for sync and async TableClient and TableServiceClient tests. * Updated test recording names. * Added new tests for entities that contain single quotes in their partition or row keys. * Fixed tables clients to support single quotes in partition keys and row keys for all operations. * Re-recorded tests. * Added a convenience method for escaping single quotes in partition keys and row keys. * Removed opt-out switch for escaping single quote characters from entity names. * Removed unused import. * Applied PR suggestions.
1 parent 1950c2e commit 5ec8d96

File tree

84 files changed

+3287
-277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+3287
-277
lines changed

sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAsyncClient.java

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ Mono<Response<TableItem>> createTableWithResponse(Context context) {
291291

292292
try {
293293
return tablesImplementation.getTables().createWithResponseAsync(properties, null,
294-
ResponseFormat.RETURN_NO_CONTENT, null, context)
294+
ResponseFormat.RETURN_NO_CONTENT, null, context)
295295
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
296296
.map(response ->
297297
new SimpleResponse<>(response,
@@ -441,7 +441,7 @@ Mono<Response<Void>> createEntityWithResponse(TableEntity entity, Context contex
441441

442442
try {
443443
return tablesImplementation.getTables().insertEntityWithResponseAsync(tableName, null, null,
444-
ResponseFormat.RETURN_NO_CONTENT, entity.getProperties(), null, context)
444+
ResponseFormat.RETURN_NO_CONTENT, entity.getProperties(), null, context)
445445
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
446446
.map(response ->
447447
new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), null));
@@ -537,21 +537,24 @@ Mono<Response<Void>> upsertEntityWithResponse(TableEntity entity, TableEntityUpd
537537
return monoError(logger, new IllegalArgumentException("'entity' cannot be null."));
538538
}
539539

540+
String partitionKey = escapeSingleQuotes(entity.getPartitionKey());
541+
String rowKey = escapeSingleQuotes(entity.getRowKey());
542+
540543
EntityHelper.setPropertiesFromGetters(entity, logger);
541544

542545
try {
543546
if (updateMode == TableEntityUpdateMode.REPLACE) {
544547
return tablesImplementation.getTables()
545-
.updateEntityWithResponseAsync(tableName, entity.getPartitionKey(), entity.getRowKey(), null,
546-
null, null, entity.getProperties(), null, context)
548+
.updateEntityWithResponseAsync(tableName, partitionKey, rowKey, null, null, null,
549+
entity.getProperties(), null, context)
547550
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
548551
.map(response ->
549552
new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(),
550553
null));
551554
} else {
552555
return tablesImplementation.getTables()
553-
.mergeEntityWithResponseAsync(tableName, entity.getPartitionKey(), entity.getRowKey(), null, null,
554-
null, entity.getProperties(), null, context)
556+
.mergeEntityWithResponseAsync(tableName, partitionKey, rowKey, null, null, null,
557+
entity.getProperties(), null, context)
555558
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
556559
.map(response ->
557560
new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(),
@@ -649,7 +652,8 @@ public Mono<Void> updateEntity(TableEntity entity, TableEntityUpdateMode updateM
649652
* <p>When the {@link TableEntityUpdateMode update mode} is {@link TableEntityUpdateMode#MERGE MERGE}, the provided
650653
* {@link TableEntity entity}'s properties will be merged into the existing {@link TableEntity entity}. When the
651654
* {@link TableEntityUpdateMode update mode} is {@link TableEntityUpdateMode#REPLACE REPLACE}, the provided
652-
* {@link TableEntity entity}'s properties will completely replace those in the existing {@link TableEntity entity}.
655+
* {@link TableEntity entity}'s properties will completely replace those in the existing
656+
* {@link TableEntity entity}.
653657
* </p>
654658
*
655659
* <p><strong>Code Samples</strong></p>
@@ -700,22 +704,25 @@ Mono<Response<Void>> updateEntityWithResponse(TableEntity entity, TableEntityUpd
700704
return monoError(logger, new IllegalArgumentException("'entity' cannot be null."));
701705
}
702706

707+
String partitionKey = escapeSingleQuotes(entity.getPartitionKey());
708+
String rowKey = escapeSingleQuotes(entity.getRowKey());
703709
String eTag = ifUnchanged ? entity.getETag() : "*";
710+
704711
EntityHelper.setPropertiesFromGetters(entity, logger);
705712

706713
try {
707714
if (updateMode == TableEntityUpdateMode.REPLACE) {
708715
return tablesImplementation.getTables()
709-
.updateEntityWithResponseAsync(tableName, entity.getPartitionKey(), entity.getRowKey(), null,
710-
null, eTag, entity.getProperties(), null, context)
716+
.updateEntityWithResponseAsync(tableName, partitionKey, rowKey, null, null, eTag,
717+
entity.getProperties(), null, context)
711718
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
712719
.map(response ->
713720
new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(),
714721
null));
715722
} else {
716723
return tablesImplementation.getTables()
717-
.mergeEntityWithResponseAsync(tableName, entity.getPartitionKey(), entity.getRowKey(), null, null,
718-
eTag, entity.getProperties(), null, context)
724+
.mergeEntityWithResponseAsync(tableName, partitionKey, rowKey, null, null, eTag,
725+
entity.getProperties(), null, context)
719726
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
720727
.map(response ->
721728
new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(),
@@ -839,8 +846,8 @@ Mono<Response<Void>> deleteEntityWithResponse(String partitionKey, String rowKey
839846
}
840847

841848
try {
842-
return tablesImplementation.getTables().deleteEntityWithResponseAsync(tableName, partitionKey, rowKey, eTag,
843-
null, null, null, context)
849+
return tablesImplementation.getTables().deleteEntityWithResponseAsync(tableName,
850+
escapeSingleQuotes(partitionKey), escapeSingleQuotes(rowKey), eTag, null, null, null, context)
844851
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
845852
.map(response -> (Response<Void>) new SimpleResponse<Void>(response, null))
846853
.onErrorResume(TableServiceException.class, e -> swallowExceptionForStatusCode(404, e, logger));
@@ -974,7 +981,7 @@ private <T extends TableEntity> Mono<PagedResponse<T>> listEntities(String nextP
974981

975982
try {
976983
return tablesImplementation.getTables().queryEntitiesWithResponseAsync(tableName, null, null,
977-
nextPartitionKey, nextRowKey, queryOptions, context)
984+
nextPartitionKey, nextRowKey, queryOptions, context)
978985
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
979986
.flatMap(response -> {
980987
final TableEntityQueryResponse tablesQueryEntityResponse = response.getValue();
@@ -1148,7 +1155,7 @@ <T extends TableEntity> Mono<Response<T>> getEntityWithResponse(String partition
11481155

11491156
try {
11501157
return tablesImplementation.getTables().queryEntityWithPartitionAndRowKeyWithResponseAsync(tableName,
1151-
partitionKey, rowKey, null, null, queryOptions, context)
1158+
escapeSingleQuotes(partitionKey), escapeSingleQuotes(rowKey), null, null, queryOptions, context)
11521159
.onErrorMap(TableUtils::mapThrowableToTableServiceException)
11531160
.handle((response, sink) -> {
11541161
final Map<String, Object> matchingEntity = response.getValue();
@@ -1742,4 +1749,14 @@ private Mono<Response<List<TableTransactionActionResponse>>> parseResponse(Trans
17421749
return Mono.just(new SimpleResponse<>(response, Arrays.asList(response.getValue())));
17431750
}
17441751
}
1752+
1753+
// Single quotes in OData queries should be escaped by using two consecutive single quotes characters.
1754+
// Source: http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_URLSyntax.
1755+
private String escapeSingleQuotes(String input) {
1756+
if (input == null) {
1757+
return null;
1758+
}
1759+
1760+
return input.replace("'", "''");
1761+
}
17451762
}

sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableEntity.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.Map;
1414
import java.util.Objects;
1515

16+
import static com.azure.core.util.CoreUtils.isNullOrEmpty;
17+
1618
/**
1719
* An entity within a table.
1820
*
@@ -41,14 +43,14 @@ public final class TableEntity {
4143
* @param rowKey The row key of the entity.
4244
*/
4345
public TableEntity(String partitionKey, String rowKey) {
44-
if (partitionKey == null || partitionKey.isEmpty()) {
45-
throw logger.logExceptionAsError(new IllegalArgumentException(String.format(
46-
"'%s' is an empty value.", TablesConstants.PARTITION_KEY)));
46+
if (isNullOrEmpty(partitionKey)) {
47+
throw logger.logExceptionAsError(
48+
new IllegalArgumentException(String.format("'%s' is an empty value.", TablesConstants.PARTITION_KEY)));
4749
}
4850

49-
if (rowKey == null || rowKey.isEmpty()) {
50-
throw logger.logExceptionAsError(new IllegalArgumentException(String.format(
51-
"'%s' is an empty value.", TablesConstants.ROW_KEY)));
51+
if (isNullOrEmpty(rowKey)) {
52+
throw logger.logExceptionAsError(
53+
new IllegalArgumentException(String.format("'%s' is an empty value.", TablesConstants.ROW_KEY)));
5254
}
5355

5456
this.properties = new HashMap<>();

0 commit comments

Comments
 (0)