Allow different data types for category in Context suggester (#23491)

The "category" in context suggester could be String, Number or Boolean. However with the changes in version 5 this is failing and only accepting String. This will have problem for existing users of Elasticsearch if they choose to migrate to higher version; as their existing Mapping and query will fail as mentioned in a bug #22358

This PR fixes the above mentioned issue and allows user to migrate seamlessly.

Closes #22358
This commit is contained in:
Nilabh Sagar 2017-04-13 12:13:29 +05:30 committed by Ryan Ernst
parent c19044ddf6
commit ec421974b9
5 changed files with 408 additions and 18 deletions

View File

@ -528,14 +528,10 @@ public class CompletionFieldMapper extends FieldMapper implements ArrayValueMapp
if (currentToken == XContentParser.Token.FIELD_NAME) {
fieldName = parser.currentName();
contextMapping = contextMappings.get(fieldName);
} else if (currentToken == XContentParser.Token.VALUE_STRING
|| currentToken == XContentParser.Token.START_ARRAY
|| currentToken == XContentParser.Token.START_OBJECT) {
} else {
assert fieldName != null;
assert !contextsMap.containsKey(fieldName);
contextsMap.put(fieldName, contextMapping.parseContext(parseContext, parser));
} else {
throw new IllegalArgumentException("contexts must be an object or an array , but was [" + currentToken + "]");
}
}
} else {

View File

@ -107,21 +107,24 @@ public class CategoryContextMapping extends ContextMapping<CategoryQueryContext>
* </ul>
*/
@Override
public Set<CharSequence> parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException {
public Set<CharSequence> parseContext(ParseContext parseContext, XContentParser parser)
throws IOException, ElasticsearchParseException {
final Set<CharSequence> contexts = new HashSet<>();
Token token = parser.currentToken();
if (token == Token.VALUE_STRING) {
if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) {
contexts.add(parser.text());
} else if (token == Token.START_ARRAY) {
while ((token = parser.nextToken()) != Token.END_ARRAY) {
if (token == Token.VALUE_STRING) {
if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) {
contexts.add(parser.text());
} else {
throw new ElasticsearchParseException("context array must have string values");
throw new ElasticsearchParseException(
"context array must have string, number or boolean values, but was [" + token + "]");
}
}
} else {
throw new ElasticsearchParseException("contexts must be a string or a list of strings");
throw new ElasticsearchParseException(
"contexts must be a string, number or boolean or a list of string, number or boolean, but was [" + token + "]");
}
return contexts;
}

View File

@ -21,6 +21,7 @@ package org.elasticsearch.search.suggest.completion.context;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -98,7 +99,8 @@ public final class CategoryQueryContext implements ToXContent {
private static ObjectParser<Builder, Void> CATEGORY_PARSER = new ObjectParser<>(NAME, null);
static {
CATEGORY_PARSER.declareString(Builder::setCategory, new ParseField(CONTEXT_VALUE));
CATEGORY_PARSER.declareField(Builder::setCategory, XContentParser::text, new ParseField(CONTEXT_VALUE),
ObjectParser.ValueType.VALUE);
CATEGORY_PARSER.declareInt(Builder::setBoost, new ParseField(CONTEXT_BOOST));
CATEGORY_PARSER.declareBoolean(Builder::setPrefix, new ParseField(CONTEXT_PREFIX));
}
@ -108,11 +110,16 @@ public final class CategoryQueryContext implements ToXContent {
XContentParser.Token token = parser.currentToken();
Builder builder = builder();
if (token == XContentParser.Token.START_OBJECT) {
CATEGORY_PARSER.parse(parser, builder, null);
} else if (token == XContentParser.Token.VALUE_STRING) {
try {
CATEGORY_PARSER.parse(parser, builder, null);
} catch(ParsingException e) {
throw new ElasticsearchParseException("category context must be a string, number or boolean");
}
} else if (token == XContentParser.Token.VALUE_STRING || token == XContentParser.Token.VALUE_BOOLEAN
|| token == XContentParser.Token.VALUE_NUMBER) {
builder.setCategory(parser.text());
} else {
throw new ElasticsearchParseException("category context must be an object or string");
throw new ElasticsearchParseException("category context must be an object, string, number or boolean");
}
return builder.build();
}

View File

@ -109,13 +109,14 @@ public abstract class ContextMapping<T extends ToXContent> implements ToXContent
List<T> queryContexts = new ArrayList<>();
XContentParser parser = context.parser();
Token token = parser.nextToken();
if (token == Token.START_OBJECT || token == Token.VALUE_STRING) {
queryContexts.add(fromXContent(context));
} else if (token == Token.START_ARRAY) {
if (token == Token.START_ARRAY) {
while (parser.nextToken() != Token.END_ARRAY) {
queryContexts.add(fromXContent(context));
}
} else {
queryContexts.add(fromXContent(context));
}
return toInternalQueryContexts(queryContexts);
}

View File

@ -23,6 +23,7 @@ import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.suggest.document.ContextSuggestField;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
@ -31,6 +32,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.MapperParsingException;
import org.elasticsearch.index.mapper.ParseContext;
import org.elasticsearch.index.mapper.ParsedDocument;
import org.elasticsearch.index.mapper.SourceToParse;
@ -120,6 +122,103 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name());
assertContextSuggestFields(fields, 3);
}
public void testIndexingWithSimpleNumberContexts() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("completion")
.field("type", "completion")
.startArray("contexts")
.startObject()
.field("name", "ctx")
.field("type", "category")
.endObject()
.endArray()
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));
FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion");
MappedFieldType completionFieldType = fieldMapper.fieldType();
ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder()
.startObject()
.startArray("completion")
.startObject()
.array("input", "suggestion5", "suggestion6", "suggestion7")
.startObject("contexts")
.field("ctx", 100)
.endObject()
.field("weight", 5)
.endObject()
.endArray()
.endObject()
.bytes());
IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name());
assertContextSuggestFields(fields, 3);
}
public void testIndexingWithSimpleBooleanContexts() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("completion")
.field("type", "completion")
.startArray("contexts")
.startObject()
.field("name", "ctx")
.field("type", "category")
.endObject()
.endArray()
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));
FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion");
MappedFieldType completionFieldType = fieldMapper.fieldType();
ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder()
.startObject()
.startArray("completion")
.startObject()
.array("input", "suggestion5", "suggestion6", "suggestion7")
.startObject("contexts")
.field("ctx", true)
.endObject()
.field("weight", 5)
.endObject()
.endArray()
.endObject()
.bytes());
IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name());
assertContextSuggestFields(fields, 3);
}
public void testIndexingWithSimpleNULLContexts() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("completion")
.field("type", "completion")
.startArray("contexts")
.startObject()
.field("name", "ctx")
.field("type", "category")
.endObject()
.endArray()
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));
XContentBuilder builder = jsonBuilder()
.startObject()
.startArray("completion")
.startObject()
.array("input", "suggestion5", "suggestion6", "suggestion7")
.startObject("contexts")
.nullField("ctx")
.endObject()
.field("weight", 5)
.endObject()
.endArray()
.endObject();
Exception e = expectThrows(MapperParsingException.class, () -> defaultMapper.parse("test", "type1", "1", builder.bytes()));
assertEquals("contexts must be a string, number or boolean or a list of string, number or boolean, but was [VALUE_NULL]", e.getCause().getMessage());
}
public void testIndexingWithContextList() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
@ -152,6 +251,66 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name());
assertContextSuggestFields(fields, 3);
}
public void testIndexingWithMixedTypeContextList() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("completion")
.field("type", "completion")
.startArray("contexts")
.startObject()
.field("name", "ctx")
.field("type", "category")
.endObject()
.endArray()
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));
FieldMapper fieldMapper = defaultMapper.mappers().getMapper("completion");
MappedFieldType completionFieldType = fieldMapper.fieldType();
ParsedDocument parsedDocument = defaultMapper.parse("test", "type1", "1", jsonBuilder()
.startObject()
.startObject("completion")
.array("input", "suggestion5", "suggestion6", "suggestion7")
.startObject("contexts")
.array("ctx", "ctx1", true, 100)
.endObject()
.field("weight", 5)
.endObject()
.endObject()
.bytes());
IndexableField[] fields = parsedDocument.rootDoc().getFields(completionFieldType.name());
assertContextSuggestFields(fields, 3);
}
public void testIndexingWithMixedTypeContextListHavingNULL() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("completion")
.field("type", "completion")
.startArray("contexts")
.startObject()
.field("name", "ctx")
.field("type", "category")
.endObject()
.endArray()
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));
XContentBuilder builder = jsonBuilder()
.startObject()
.startObject("completion")
.array("input", "suggestion5", "suggestion6", "suggestion7")
.startObject("contexts")
.array("ctx", "ctx1", true, 100, null)
.endObject()
.field("weight", 5)
.endObject()
.endObject();
Exception e = expectThrows(MapperParsingException.class, () -> defaultMapper.parse("test", "type1", "1", builder.bytes()));
assertEquals("context array must have string, number or boolean values, but was [VALUE_NULL]", e.getCause().getMessage());
}
public void testIndexingWithMultipleContexts() throws Exception {
String mapping = jsonBuilder().startObject().startObject("type1")
@ -202,6 +361,37 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
assertThat(internalQueryContexts.get(0).boost, equalTo(1));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false));
}
public void testBooleanQueryContextParsingBasic() throws Exception {
XContentBuilder builder = jsonBuilder().value(true);
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(1));
assertThat(internalQueryContexts.get(0).context, equalTo("true"));
assertThat(internalQueryContexts.get(0).boost, equalTo(1));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false));
}
public void testNumberQueryContextParsingBasic() throws Exception {
XContentBuilder builder = jsonBuilder().value(10);
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(1));
assertThat(internalQueryContexts.get(0).context, equalTo("10"));
assertThat(internalQueryContexts.get(0).boost, equalTo(1));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false));
}
public void testNULLQueryContextParsingBasic() throws Exception {
XContentBuilder builder = jsonBuilder().nullValue();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser)));
assertEquals("category context must be an object, string, number or boolean", e.getMessage());
}
public void testQueryContextParsingArray() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
@ -219,6 +409,46 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
assertThat(internalQueryContexts.get(1).boost, equalTo(1));
assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false));
}
public void testQueryContextParsingMixedTypeValuesArray() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.value("context1")
.value("context2")
.value(true)
.value(10)
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(4));
assertThat(internalQueryContexts.get(0).context, equalTo("context1"));
assertThat(internalQueryContexts.get(0).boost, equalTo(1));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(1).context, equalTo("context2"));
assertThat(internalQueryContexts.get(1).boost, equalTo(1));
assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(2).context, equalTo("true"));
assertThat(internalQueryContexts.get(2).boost, equalTo(1));
assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(3).context, equalTo("10"));
assertThat(internalQueryContexts.get(3).boost, equalTo(1));
assertThat(internalQueryContexts.get(3).isPrefix, equalTo(false));
}
public void testQueryContextParsingMixedTypeValuesArrayHavingNULL() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.value("context1")
.value("context2")
.value(true)
.value(10)
.nullValue()
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser)));
assertEquals("category context must be an object, string, number or boolean", e.getMessage());
}
public void testQueryContextParsingObject() throws Exception {
XContentBuilder builder = jsonBuilder().startObject()
@ -235,7 +465,49 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true));
}
public void testQueryContextParsingObjectHavingBoolean() throws Exception {
XContentBuilder builder = jsonBuilder().startObject()
.field("context", false)
.field("boost", 10)
.field("prefix", true)
.endObject();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(1));
assertThat(internalQueryContexts.get(0).context, equalTo("false"));
assertThat(internalQueryContexts.get(0).boost, equalTo(10));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true));
}
public void testQueryContextParsingObjectHavingNumber() throws Exception {
XContentBuilder builder = jsonBuilder().startObject()
.field("context", 333)
.field("boost", 10)
.field("prefix", true)
.endObject();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(1));
assertThat(internalQueryContexts.get(0).context, equalTo("333"));
assertThat(internalQueryContexts.get(0).boost, equalTo(10));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true));
}
public void testQueryContextParsingObjectHavingNULL() throws Exception {
XContentBuilder builder = jsonBuilder().startObject()
.nullField("context")
.field("boost", 10)
.field("prefix", true)
.endObject();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser)));
assertEquals("category context must be a string, number or boolean", e.getMessage());
}
public void testQueryContextParsingObjectArray() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.startObject()
@ -260,6 +532,82 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
assertThat(internalQueryContexts.get(1).boost, equalTo(3));
assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false));
}
public void testQueryContextParsingMixedTypeObjectArray() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.startObject()
.field("context", "context1")
.field("boost", 2)
.field("prefix", true)
.endObject()
.startObject()
.field("context", "context2")
.field("boost", 3)
.field("prefix", false)
.endObject()
.startObject()
.field("context", true)
.field("boost", 3)
.field("prefix", false)
.endObject()
.startObject()
.field("context", 333)
.field("boost", 3)
.field("prefix", false)
.endObject()
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(4));
assertThat(internalQueryContexts.get(0).context, equalTo("context1"));
assertThat(internalQueryContexts.get(0).boost, equalTo(2));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true));
assertThat(internalQueryContexts.get(1).context, equalTo("context2"));
assertThat(internalQueryContexts.get(1).boost, equalTo(3));
assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(2).context, equalTo("true"));
assertThat(internalQueryContexts.get(2).boost, equalTo(3));
assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(3).context, equalTo("333"));
assertThat(internalQueryContexts.get(3).boost, equalTo(3));
assertThat(internalQueryContexts.get(3).isPrefix, equalTo(false));
}
public void testQueryContextParsingMixedTypeObjectArrayHavingNULL() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.startObject()
.field("context", "context1")
.field("boost", 2)
.field("prefix", true)
.endObject()
.startObject()
.field("context", "context2")
.field("boost", 3)
.field("prefix", false)
.endObject()
.startObject()
.field("context", true)
.field("boost", 3)
.field("prefix", false)
.endObject()
.startObject()
.field("context", 333)
.field("boost", 3)
.field("prefix", false)
.endObject()
.startObject()
.nullField("context")
.field("boost", 3)
.field("prefix", false)
.endObject()
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser)));
assertEquals("category context must be a string, number or boolean", e.getMessage());
}
private static QueryParseContext createParseContext(XContentParser parser) {
return new QueryParseContext(parser);
@ -273,17 +621,52 @@ public class CategoryContextMappingTests extends ESSingleNodeTestCase {
.field("prefix", true)
.endObject()
.value("context2")
.value(false)
.startObject()
.field("context", 333)
.field("boost", 2)
.field("prefix", true)
.endObject()
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
List<ContextMapping.InternalQueryContext> internalQueryContexts = mapping.parseQueryContext(createParseContext(parser));
assertThat(internalQueryContexts.size(), equalTo(2));
assertThat(internalQueryContexts.size(), equalTo(4));
assertThat(internalQueryContexts.get(0).context, equalTo("context1"));
assertThat(internalQueryContexts.get(0).boost, equalTo(2));
assertThat(internalQueryContexts.get(0).isPrefix, equalTo(true));
assertThat(internalQueryContexts.get(1).context, equalTo("context2"));
assertThat(internalQueryContexts.get(1).boost, equalTo(1));
assertThat(internalQueryContexts.get(1).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(2).context, equalTo("false"));
assertThat(internalQueryContexts.get(2).boost, equalTo(1));
assertThat(internalQueryContexts.get(2).isPrefix, equalTo(false));
assertThat(internalQueryContexts.get(3).context, equalTo("333"));
assertThat(internalQueryContexts.get(3).boost, equalTo(2));
assertThat(internalQueryContexts.get(3).isPrefix, equalTo(true));
}
public void testQueryContextParsingMixedHavingNULL() throws Exception {
XContentBuilder builder = jsonBuilder().startArray()
.startObject()
.field("context", "context1")
.field("boost", 2)
.field("prefix", true)
.endObject()
.value("context2")
.value(false)
.startObject()
.field("context", 333)
.field("boost", 2)
.field("prefix", true)
.endObject()
.nullValue()
.endArray();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
CategoryContextMapping mapping = ContextBuilder.category("cat").build();
Exception e = expectThrows(ElasticsearchParseException.class, () -> mapping.parseQueryContext(createParseContext(parser)));
assertEquals("category context must be an object, string, number or boolean", e.getMessage());
}
public void testParsingContextFromDocument() throws Exception {