Skip to content

Commit 198b2d6

Browse files
authored
ParametrizedFieldMapper to run validators against default value (#60042)
Sometimes there is the need to make a field required in the mappings, and validate that a value has been provided for it. This can be done through a validator when using ParametrizedFieldMapper, but validators need to run also when a value for a field has not been specified. Relates to #59332
1 parent 6f5bbcc commit 198b2d6

File tree

2 files changed

+63
-39
lines changed

2 files changed

+63
-39
lines changed

server/src/main/java/org/elasticsearch/index/mapper/ParametrizedFieldMapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ public Parameter<T> setSerializer(Serializer<T> serializer) {
218218
}
219219

220220
private void validate() {
221-
if (validator != null && isSet) {
222-
validator.accept(value);
221+
if (validator != null) {
222+
validator.accept(getValue());
223223
}
224224
}
225225

server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,21 @@ public static class Builder extends ParametrizedFieldMapper.Builder {
114114
= Parameter.analyzerParam("analyzer", true, m -> toType(m).analyzer, () -> Lucene.KEYWORD_ANALYZER);
115115
final Parameter<NamedAnalyzer> searchAnalyzer
116116
= Parameter.analyzerParam("search_analyzer", true, m -> toType(m).searchAnalyzer, analyzer::getValue);
117-
118117
final Parameter<Boolean> index = Parameter.boolParam("index", false, m -> toType(m).index, true);
118+
final Parameter<String> required = Parameter.stringParam("required", true, m -> toType(m).required, null)
119+
.setValidator(value -> {
120+
if (value == null) {
121+
throw new IllegalArgumentException("field [required] must be specified");
122+
}
123+
});
119124

120125
protected Builder(String name) {
121126
super(name);
122127
}
123128

124129
@Override
125130
protected List<Parameter<?>> getParameters() {
126-
return List.of(fixed, fixed2, variable, index, wrapper, intValue, analyzer, searchAnalyzer);
131+
return List.of(fixed, fixed2, variable, index, wrapper, intValue, analyzer, searchAnalyzer, required);
127132
}
128133

129134
@Override
@@ -153,6 +158,7 @@ public static class TestMapper extends ParametrizedFieldMapper {
153158
private final NamedAnalyzer analyzer;
154159
private final NamedAnalyzer searchAnalyzer;
155160
private final boolean index;
161+
private final String required;
156162

157163
protected TestMapper(String simpleName, String fullName, MultiFields multiFields, CopyTo copyTo,
158164
ParametrizedMapperTests.Builder builder) {
@@ -165,6 +171,7 @@ protected TestMapper(String simpleName, String fullName, MultiFields multiFields
165171
this.analyzer = builder.analyzer.getValue();
166172
this.searchAnalyzer = builder.searchAnalyzer.getValue();
167173
this.index = builder.index.getValue();
174+
this.required = builder.required.getValue();
168175
}
169176

170177
@Override
@@ -173,7 +180,7 @@ public Builder getMergeBuilder() {
173180
}
174181

175182
@Override
176-
protected void parseCreateField(ParseContext context) throws IOException {
183+
protected void parseCreateField(ParseContext context) {
177184

178185
}
179186

@@ -211,7 +218,7 @@ private static TestMapper fromMapping(String mapping) {
211218

212219
// defaults - create empty builder config, and serialize with and without defaults
213220
public void testDefaults() throws IOException {
214-
String mapping = "{\"type\":\"test_mapper\"}";
221+
String mapping = "{\"type\":\"test_mapper\",\"required\":\"value\"}";
215222
TestMapper mapper = fromMapping(mapping);
216223

217224
assertTrue(mapper.fixed);
@@ -226,17 +233,18 @@ public void testDefaults() throws IOException {
226233
builder.endObject();
227234
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed\":true," +
228235
"\"fixed2\":false,\"variable\":\"default\",\"index\":true," +
229-
"\"wrapper\":\"default\",\"int_value\":5,\"analyzer\":\"_keyword\",\"search_analyzer\":\"_keyword\"}}",
236+
"\"wrapper\":\"default\",\"int_value\":5,\"analyzer\":\"_keyword\"," +
237+
"\"search_analyzer\":\"_keyword\",\"required\":\"value\"}}",
230238
Strings.toString(builder));
231239
}
232240

233241
// merging - try updating 'fixed' and 'fixed2' should get an error, try updating 'variable' and verify update
234242
public void testMerging() {
235-
String mapping = "{\"type\":\"test_mapper\",\"fixed\":false}";
243+
String mapping = "{\"type\":\"test_mapper\",\"fixed\":false,\"required\":\"value\"}";
236244
TestMapper mapper = fromMapping(mapping);
237245
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
238246

239-
TestMapper badMerge = fromMapping("{\"type\":\"test_mapper\",\"fixed\":true,\"fixed2\":true}");
247+
TestMapper badMerge = fromMapping("{\"type\":\"test_mapper\",\"fixed\":true,\"fixed2\":true,\"required\":\"value\"}");
240248
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> mapper.merge(badMerge));
241249
String expectedError = "Mapper for [field] conflicts with existing mapper:\n" +
242250
"\tCannot update parameter [fixed] from [false] to [true]\n" +
@@ -246,56 +254,61 @@ public void testMerging() {
246254
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper)); // original mapping is unaffected
247255

248256
// TODO: should we have to include 'fixed' here? Or should updates take as 'defaults' the existing values?
249-
TestMapper goodMerge = fromMapping("{\"type\":\"test_mapper\",\"fixed\":false,\"variable\":\"updated\"}");
257+
TestMapper goodMerge = fromMapping("{\"type\":\"test_mapper\",\"fixed\":false,\"variable\":\"updated\",\"required\":\"value\"}");
250258
TestMapper merged = (TestMapper) mapper.merge(goodMerge);
251259

252260
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper)); // original mapping is unaffected
253-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed\":false,\"variable\":\"updated\"}}", Strings.toString(merged));
254-
261+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed\":false,\"variable\":\"updated\",\"required\":\"value\"}}",
262+
Strings.toString(merged));
255263
}
256264

257265
// add multifield, verify, add second multifield, verify, overwrite second multifield
258266
public void testMultifields() {
259-
String mapping = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"fields\":{\"sub\":{\"type\":\"keyword\"}}}";
267+
String mapping = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"required\":\"value\"," +
268+
"\"fields\":{\"sub\":{\"type\":\"keyword\"}}}";
260269
TestMapper mapper = fromMapping(mapping);
261270
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
262271

263-
String addSubField = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"fields\":{\"sub2\":{\"type\":\"keyword\"}}}";
272+
String addSubField = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"required\":\"value\"" +
273+
",\"fields\":{\"sub2\":{\"type\":\"keyword\"}}}";
264274
TestMapper toMerge = fromMapping(addSubField);
265275
TestMapper merged = (TestMapper) mapper.merge(toMerge);
266-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"foo\"," +
276+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"foo\",\"required\":\"value\"," +
267277
"\"fields\":{\"sub\":{\"type\":\"keyword\"},\"sub2\":{\"type\":\"keyword\"}}}}", Strings.toString(merged));
268278

269-
String badSubField = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"fields\":{\"sub2\":{\"type\":\"binary\"}}}";
279+
String badSubField = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"required\":\"value\"," +
280+
"\"fields\":{\"sub2\":{\"type\":\"binary\"}}}";
270281
TestMapper badToMerge = fromMapping(badSubField);
271282
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> merged.merge(badToMerge));
272283
assertEquals("mapper [field.sub2] cannot be changed from type [keyword] to [binary]", e.getMessage());
273284
}
274285

275286
// add copy_to, verify
276287
public void testCopyTo() {
277-
String mapping = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"copy_to\":[\"other\"]}";
288+
String mapping = "{\"type\":\"test_mapper\",\"variable\":\"foo\",\"required\":\"value\",\"copy_to\":[\"other\"]}";
278289
TestMapper mapper = fromMapping(mapping);
279290
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
280291

281292
// On update, copy_to is completely replaced
282293

283-
TestMapper toMerge = fromMapping("{\"type\":\"test_mapper\",\"variable\":\"updated\",\"copy_to\":[\"foo\",\"bar\"]}");
294+
TestMapper toMerge = fromMapping("{\"type\":\"test_mapper\",\"variable\":\"updated\",\"required\":\"value\"," +
295+
"\"copy_to\":[\"foo\",\"bar\"]}");
284296
TestMapper merged = (TestMapper) mapper.merge(toMerge);
285-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"updated\",\"copy_to\":[\"foo\",\"bar\"]}}",
297+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"updated\",\"required\":\"value\"," +
298+
"\"copy_to\":[\"foo\",\"bar\"]}}",
286299
Strings.toString(merged));
287300

288-
TestMapper removeCopyTo = fromMapping("{\"type\":\"test_mapper\",\"variable\":\"updated\"}");
301+
TestMapper removeCopyTo = fromMapping("{\"type\":\"test_mapper\",\"variable\":\"updated\",\"required\":\"value\"}");
289302
TestMapper noCopyTo = (TestMapper) merged.merge(removeCopyTo);
290-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"updated\"}}", Strings.toString(noCopyTo));
303+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"variable\":\"updated\",\"required\":\"value\"}}", Strings.toString(noCopyTo));
291304
}
292305

293306
public void testNullables() {
294-
String mapping = "{\"type\":\"test_mapper\",\"fixed\":null}";
307+
String mapping = "{\"type\":\"test_mapper\",\"fixed\":null,\"required\":\"value\"}";
295308
MapperParsingException e = expectThrows(MapperParsingException.class, () -> fromMapping(mapping));
296309
assertEquals("[fixed] on mapper [field] of type [test_mapper] must not have a [null] value", e.getMessage());
297310

298-
String fine = "{\"type\":\"test_mapper\",\"variable\":null}";
311+
String fine = "{\"type\":\"test_mapper\",\"variable\":null,\"required\":\"value\"}";
299312
TestMapper mapper = fromMapping(fine);
300313
assertEquals("{\"field\":" + fine + "}", Strings.toString(mapper));
301314
}
@@ -321,82 +334,93 @@ public void testObjectSerialization() throws IOException {
321334

322335
// test custom serializer
323336
public void testCustomSerialization() {
324-
String mapping = "{\"type\":\"test_mapper\",\"wrapper\":\"wrapped value\"}";
337+
String mapping = "{\"type\":\"test_mapper\",\"wrapper\":\"wrapped value\",\"required\":\"value\"}";
325338
TestMapper mapper = fromMapping(mapping);
326339
assertEquals("wrapped value", mapper.wrapper.name);
327340
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
328341
}
329342

330343
// test validator
331344
public void testParameterValidation() {
332-
String mapping = "{\"type\":\"test_mapper\",\"int_value\":10}";
345+
String mapping = "{\"type\":\"test_mapper\",\"int_value\":10,\"required\":\"value\"}";
333346
TestMapper mapper = fromMapping(mapping);
334347
assertEquals(10, mapper.intValue);
335348
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
336349

337350
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
338-
() -> fromMapping("{\"type\":\"test_mapper\",\"int_value\":60}"));
351+
() -> fromMapping("{\"type\":\"test_mapper\",\"int_value\":60,\"required\":\"value\"}"));
339352
assertEquals("Value of [n] cannot be greater than 50", e.getMessage());
340-
341353
}
342354

343355
// test deprecations
344356
public void testDeprecatedParameterName() {
345-
String mapping = "{\"type\":\"test_mapper\",\"fixed2_old\":true}";
357+
String mapping = "{\"type\":\"test_mapper\",\"fixed2_old\":true,\"required\":\"value\"}";
346358
TestMapper mapper = fromMapping(mapping);
347359
assertTrue(mapper.fixed2);
348360
assertWarnings("Parameter [fixed2_old] on mapper [field] is deprecated, use [fixed2]");
349-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed2\":true}}", Strings.toString(mapper));
361+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed2\":true,\"required\":\"value\"}}", Strings.toString(mapper));
350362
}
351363

352364
public void testAnalyzers() {
353-
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\"}";
365+
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\",\"required\":\"value\"}";
354366
TestMapper mapper = fromMapping(mapping);
355367
assertEquals(mapper.analyzer, Lucene.STANDARD_ANALYZER);
356368
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
357369

358-
String withDef = "{\"type\":\"test_mapper\",\"analyzer\":\"default\"}";
370+
String withDef = "{\"type\":\"test_mapper\",\"analyzer\":\"default\",\"required\":\"value\"}";
359371
mapper = fromMapping(withDef);
360372
assertEquals(mapper.analyzer.name(), "default");
361373
assertThat(mapper.analyzer.analyzer(), instanceOf(StandardAnalyzer.class));
362374
assertEquals("{\"field\":" + withDef + "}", Strings.toString(mapper));
363375

364-
String badAnalyzer = "{\"type\":\"test_mapper\",\"analyzer\":\"wibble\"}";
376+
String badAnalyzer = "{\"type\":\"test_mapper\",\"analyzer\":\"wibble\",\"required\":\"value\"}";
365377
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> fromMapping(badAnalyzer));
366378
assertEquals("analyzer [wibble] has not been configured in mappings", e.getMessage());
367379
}
368380

369-
public void testDeprecatedParameters() throws IOException {
381+
public void testDeprecatedParameters() {
370382
// 'index' is declared explicitly, 'store' is not, but is one of the previously always-accepted params
371-
String mapping = "{\"type\":\"test_mapper\",\"index\":false,\"store\":true}";
383+
String mapping = "{\"type\":\"test_mapper\",\"index\":false,\"store\":true,\"required\":\"value\"}";
372384
TestMapper mapper = fromMapping(mapping, Version.V_7_8_0);
373385
assertWarnings("Parameter [store] has no effect on type [test_mapper] and will be removed in future");
374386
assertFalse(mapper.index);
375-
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"index\":false}}", Strings.toString(mapper));
387+
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"index\":false,\"required\":\"value\"}}", Strings.toString(mapper));
376388

377389
MapperParsingException e = expectThrows(MapperParsingException.class, () -> fromMapping(mapping, Version.V_8_0_0));
378390
assertEquals("unknown parameter [store] on mapper [field] of type [test_mapper]", e.getMessage());
379391
}
380392

381393
public void testLinkedAnalyzers() {
382-
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\"}";
394+
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\",\"required\":\"value\"}";
383395
TestMapper mapper = fromMapping(mapping);
384396
assertEquals("_standard", mapper.analyzer.name());
385397
assertEquals("_standard", mapper.searchAnalyzer.name());
386398
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
387399

388-
String mappingWithSA = "{\"type\":\"test_mapper\",\"search_analyzer\":\"_standard\"}";
400+
String mappingWithSA = "{\"type\":\"test_mapper\",\"search_analyzer\":\"_standard\",\"required\":\"value\"}";
389401
mapper = fromMapping(mappingWithSA);
390402
assertEquals("_keyword", mapper.analyzer.name());
391403
assertEquals("_standard", mapper.searchAnalyzer.name());
392404
assertEquals("{\"field\":" + mappingWithSA + "}", Strings.toString(mapper));
393405

394-
String mappingWithBoth = "{\"type\":\"test_mapper\",\"analyzer\":\"default\",\"search_analyzer\":\"_standard\"}";
406+
String mappingWithBoth = "{\"type\":\"test_mapper\",\"analyzer\":\"default\"," +
407+
"\"search_analyzer\":\"_standard\",\"required\":\"value\"}";
395408
mapper = fromMapping(mappingWithBoth);
396409
assertEquals("default", mapper.analyzer.name());
397410
assertEquals("_standard", mapper.searchAnalyzer.name());
398411
assertEquals("{\"field\":" + mappingWithBoth + "}", Strings.toString(mapper));
399-
400412
}
401413

414+
public void testRequiredField() {
415+
{
416+
String mapping = "{\"type\":\"test_mapper\"}";
417+
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> fromMapping(mapping));
418+
assertEquals("field [required] must be specified", iae.getMessage());
419+
}
420+
{
421+
String mapping = "{\"type\":\"test_mapper\",\"required\":null}";
422+
MapperParsingException exc = expectThrows(MapperParsingException.class, () -> fromMapping(mapping));
423+
assertEquals("[required] on mapper [field] of type [test_mapper] must not have a [null] value", exc.getMessage());
424+
}
425+
}
402426
}

0 commit comments

Comments
 (0)