Skip to content

Commit 6693493

Browse files
authored
Improve error message for invalid field names (#985)
For replacement documents, it's now: "Field names in a replacement document can not start with '$' but '%s' does" For update documents, it's now: "All update operators must start with '$', but '%s' does not" JAVA-2114
1 parent a8b8976 commit 6693493

10 files changed

+90
-15
lines changed

bson/src/main/org/bson/AbstractBsonWriter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,9 @@ public void writeName(final String name) {
530530
if (state != State.NAME) {
531531
throwInvalidState("WriteName", State.NAME);
532532
}
533-
if (!fieldNameValidatorStack.peek().validate(name)) {
534-
throw new IllegalArgumentException(format("Invalid BSON field name %s", name));
533+
FieldNameValidator fieldNameValidator = fieldNameValidatorStack.peek();
534+
if (!fieldNameValidator.validate(name)) {
535+
throw new IllegalArgumentException(fieldNameValidator.getValidationErrorMessage(name));
535536
}
536537
doWriteName(name);
537538
context.name = name;

bson/src/main/org/bson/FieldNameValidator.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.bson;
1818

19+
import static java.lang.String.format;
20+
import static org.bson.assertions.Assertions.isTrue;
21+
1922
/**
2023
* A field name validator, for use by BSON writers to validate field names as documents are encoded.
2124
*
@@ -30,6 +33,18 @@ public interface FieldNameValidator {
3033
*/
3134
boolean validate(String fieldName);
3235

36+
/**
37+
* Return the validation error message for an invalid field
38+
*
39+
* @param fieldName the field name
40+
* @return the validation error message
41+
* @throws IllegalArgumentException if fieldName is actually valid
42+
*/
43+
default String getValidationErrorMessage(final String fieldName) {
44+
isTrue(fieldName + " is valid", !validate(fieldName));
45+
return format("Invalid BSON field name %s", fieldName);
46+
}
47+
3348
/**
3449
* Gets a new validator to use for the value of the field with the given name.
3550
*

bson/src/test/unit/org/bson/BsonWriterSpecification.groovy

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ class BsonWriterSpecification extends Specification {
384384
writer.writeString('bad-child', 'string')
385385

386386
then:
387-
thrown(IllegalArgumentException)
387+
def e = thrown(IllegalArgumentException)
388+
e.getMessage() == 'testFieldNameValidator error'
388389

389390
where:
390391
writer << [new BsonBinaryWriter(new BasicOutputBuffer(), new TestFieldNameValidator('bad'))]
@@ -402,6 +403,11 @@ class BsonWriterSpecification extends Specification {
402403
fieldName != badFieldName
403404
}
404405

406+
@Override
407+
String getValidationErrorMessage(final String fieldName) {
408+
'testFieldNameValidator error'
409+
}
410+
405411
@Override
406412
FieldNameValidator getValidatorForField(final String fieldName) {
407413
new TestFieldNameValidator(badFieldName + '-child')

driver-core/src/main/com/mongodb/internal/validator/MappedFieldNameValidator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public boolean validate(final String fieldName) {
4848
return defaultValidator.validate(fieldName);
4949
}
5050

51+
@Override
52+
public String getValidationErrorMessage(final String fieldName) {
53+
return defaultValidator.getValidationErrorMessage(fieldName);
54+
}
55+
5156
@Override
5257
public FieldNameValidator getValidatorForField(final String fieldName) {
5358
if (fieldNameToValidatorMap.containsKey(fieldName)) {

driver-core/src/main/com/mongodb/internal/validator/ReplacingDocumentFieldNameValidator.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import java.util.Arrays;
2222
import java.util.List;
2323

24+
import static com.mongodb.assertions.Assertions.assertFalse;
25+
import static java.lang.String.format;
26+
2427
/**
2528
* A field name validator for documents that are meant for storage in MongoDB collections via replace operations. It ensures that no
2629
* top-level fields start with '$' (with the exception of "$db", "$ref", and "$id", so that DBRefs are not rejected).
@@ -34,13 +37,15 @@ public class ReplacingDocumentFieldNameValidator implements FieldNameValidator {
3437

3538
@Override
3639
public boolean validate(final String fieldName) {
37-
if (fieldName == null) {
38-
throw new IllegalArgumentException("Field name can not be null");
39-
}
40-
4140
return !fieldName.startsWith("$") || EXCEPTIONS.contains(fieldName);
4241
}
4342

43+
@Override
44+
public String getValidationErrorMessage(final String fieldName) {
45+
assertFalse(validate(fieldName));
46+
return format("Field names in a replacement document can not start with '$' but '%s' does", fieldName);
47+
}
48+
4449
@Override
4550
public FieldNameValidator getValidatorForField(final String fieldName) {
4651
// Only top-level fields are validated

driver-core/src/main/com/mongodb/internal/validator/UpdateFieldNameValidator.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import org.bson.FieldNameValidator;
2020

21+
import static com.mongodb.assertions.Assertions.assertFalse;
22+
import static java.lang.String.format;
23+
2124
/**
2225
* A field name validator for update documents. It ensures that all top-level fields start with a '$'.
2326
*
@@ -32,6 +35,12 @@ public boolean validate(final String fieldName) {
3235
return fieldName.startsWith("$");
3336
}
3437

38+
@Override
39+
public String getValidationErrorMessage(final String fieldName) {
40+
assertFalse(fieldName.startsWith("$"));
41+
return format("All update operators must start with '$', but '%s' does not", fieldName);
42+
}
43+
3544
@Override
3645
public FieldNameValidator getValidatorForField(final String fieldName) {
3746
return new NoOpFieldNameValidator();

driver-core/src/test/functional/com/mongodb/internal/operation/FindAndReplaceOperationSpecification.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ class FindAndReplaceOperationSpecification extends OperationFunctionalSpecificat
189189
execute(operation, async)
190190

191191
then:
192-
thrown(IllegalArgumentException)
192+
def e = thrown(IllegalArgumentException)
193+
e.getMessage() == 'Field names in a replacement document can not start with \'$\' but \'$inc\' does'
193194

194195
where:
195196
async << [true, false]

driver-core/src/test/functional/com/mongodb/internal/operation/FindAndUpdateOperationSpecification.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ class FindAndUpdateOperationSpecification extends OperationFunctionalSpecificati
293293
execute(operation, async)
294294

295295
then:
296-
thrown(IllegalArgumentException)
296+
def e = thrown(IllegalArgumentException)
297+
e.getMessage() == 'All update operators must start with \'$\', but \'x\' does not'
297298

298299
where:
299300
async << [true, false]

driver-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,44 @@ class MixedBulkWriteOperationSpecification extends OperationFunctionalSpecificat
352352
execute(operation, async)
353353

354354
then:
355-
thrown(IllegalArgumentException)
355+
def e = thrown(IllegalArgumentException)
356+
e.getMessage() == 'All update operators must start with \'$\', but \'a\' does not'
357+
358+
where:
359+
[async, ordered] << [[true, false], [true, false]].combinations()
360+
}
361+
362+
def 'when replacing an invalid document, replace should throw IllegalArgumentException'() {
363+
given:
364+
def id = new ObjectId()
365+
def operation = new MixedBulkWriteOperation(getNamespace(),
366+
[new UpdateRequest(new BsonDocument('_id', new BsonObjectId(id)),
367+
new BsonDocument('$set', new BsonDocument('x', new BsonInt32(1))), REPLACE)],
368+
true, ACKNOWLEDGED, false)
369+
370+
when:
371+
execute(operation, async)
372+
373+
then:
374+
def e = thrown(IllegalArgumentException)
375+
e.getMessage() == 'Field names in a replacement document can not start with \'$\' but \'$set\' does'
376+
377+
where:
378+
[async, ordered] << [[true, false], [true, false]].combinations()
379+
}
380+
381+
@IgnoreIf({ serverVersionLessThan(5, 0) })
382+
def 'when inserting a document with a field starting with a dollar sign, insert should not throw'() {
383+
given:
384+
def operation = new MixedBulkWriteOperation(getNamespace(),
385+
[new InsertRequest(new BsonDocument('$inc', new BsonDocument('x', new BsonInt32(1))))],
386+
true, ACKNOWLEDGED, false)
387+
388+
when:
389+
execute(operation, async)
390+
391+
then:
392+
notThrown(IllegalArgumentException)
356393

357394
where:
358395
[async, ordered] << [[true, false], [true, false]].combinations()

driver-core/src/test/unit/com/mongodb/internal/validator/ReplacingDocumentFieldNameValidatorTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ public void testFieldValidationSuccess() {
3030
assertTrue(fieldNameValidator.validate("ok"));
3131
}
3232

33-
@Test(expected = IllegalArgumentException.class)
34-
public void testNullFieldNameValidation() {
35-
fieldNameValidator.validate(null);
36-
}
37-
3833
@Test
3934
public void testFieldNameStartsWithDollarValidation() {
4035
assertFalse(fieldNameValidator.validate("$1"));

0 commit comments

Comments
 (0)