Skip to content

Commit be96278

Browse files
authored
Add parsing method for ElasticsearchException.generateThrowableXContent() (#22783)
The output of the ElasticsearchException.generateThrowableXContent() method can be parsed back by the ElasticsearchException.fromXContent() method. This commit adds unit tests in the style of the to-and-from-xcontent tests we already have for other parsing methods. It also relax the strict parsing of the ElasticsearchException.fromXContent() so that it does not throw an exception when custom metadata and headers are parsed, as long as they are either strings or arrays of strings. Every other type is ignored at parsing time.
1 parent f128b7a commit be96278

File tree

3 files changed

+502
-67
lines changed

3 files changed

+502
-67
lines changed

core/src/main/java/org/elasticsearch/ElasticsearchException.java

Lines changed: 115 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.transport.TcpTransport;
3737

3838
import java.io.IOException;
39+
import java.util.ArrayList;
3940
import java.util.Arrays;
4041
import java.util.Collections;
4142
import java.util.HashMap;
@@ -50,7 +51,6 @@
5051
import static java.util.Collections.unmodifiableMap;
5152
import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_UUID_NA_VALUE;
5253
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
53-
import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField;
5454

5555
/**
5656
* A base class for all elasticsearch exceptions.
@@ -391,12 +391,125 @@ private static void headerToXContent(XContentBuilder builder, String key, List<S
391391
protected void metadataToXContent(XContentBuilder builder, Params params) throws IOException {
392392
}
393393

394+
/**
395+
* Generate a {@link ElasticsearchException} from a {@link XContentParser}. This does not
396+
* return the original exception type (ie NodeClosedException for example) but just wraps
397+
* the type, the reason and the cause of the exception. It also recursively parses the
398+
* tree structure of the cause, returning it as a tree structure of {@link ElasticsearchException}
399+
* instances.
400+
*/
401+
public static ElasticsearchException fromXContent(XContentParser parser) throws IOException {
402+
XContentParser.Token token = parser.nextToken();
403+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
404+
return innerFromXContent(parser);
405+
}
406+
407+
private static ElasticsearchException innerFromXContent(XContentParser parser) throws IOException {
408+
XContentParser.Token token = parser.currentToken();
409+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
410+
411+
String type = null, reason = null, stack = null;
412+
ElasticsearchException cause = null;
413+
Map<String, List<String>> metadata = new HashMap<>();
414+
Map<String, List<String>> headers = new HashMap<>();
415+
416+
for (; token == XContentParser.Token.FIELD_NAME; token = parser.nextToken()) {
417+
String currentFieldName = parser.currentName();
418+
token = parser.nextToken();
419+
420+
if (token.isValue()) {
421+
if (TYPE.equals(currentFieldName)) {
422+
type = parser.text();
423+
} else if (REASON.equals(currentFieldName)) {
424+
reason = parser.text();
425+
} else if (STACK_TRACE.equals(currentFieldName)) {
426+
stack = parser.text();
427+
} else if (token == XContentParser.Token.VALUE_STRING) {
428+
metadata.put(currentFieldName, Collections.singletonList(parser.text()));
429+
}
430+
} else if (token == XContentParser.Token.START_OBJECT) {
431+
if (CAUSED_BY.equals(currentFieldName)) {
432+
cause = fromXContent(parser);
433+
} else if (HEADER.equals(currentFieldName)) {
434+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
435+
if (token == XContentParser.Token.FIELD_NAME) {
436+
currentFieldName = parser.currentName();
437+
} else {
438+
List<String> values = headers.getOrDefault(currentFieldName, new ArrayList<>());
439+
if (token == XContentParser.Token.VALUE_STRING) {
440+
values.add(parser.text());
441+
} else if (token == XContentParser.Token.START_ARRAY) {
442+
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
443+
if (token == XContentParser.Token.VALUE_STRING) {
444+
values.add(parser.text());
445+
} else {
446+
parser.skipChildren();
447+
}
448+
}
449+
} else if (token == XContentParser.Token.START_OBJECT) {
450+
parser.skipChildren();
451+
}
452+
headers.put(currentFieldName, values);
453+
}
454+
}
455+
} else {
456+
// Any additional metadata object added by the metadataToXContent method is ignored
457+
// and skipped, so that the parser does not fail on unknown fields. The parser only
458+
// support metadata key-pairs and metadata arrays of values.
459+
parser.skipChildren();
460+
}
461+
} else if (token == XContentParser.Token.START_ARRAY) {
462+
// Parse the array and add each item to the corresponding list of metadata.
463+
// Arrays of objects are not supported yet and just ignored and skipped.
464+
List<String> values = new ArrayList<>();
465+
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
466+
if (token == XContentParser.Token.VALUE_STRING) {
467+
values.add(parser.text());
468+
} else {
469+
parser.skipChildren();
470+
}
471+
}
472+
if (values.size() > 0) {
473+
if (metadata.containsKey(currentFieldName)) {
474+
values.addAll(metadata.get(currentFieldName));
475+
}
476+
metadata.put(currentFieldName, values);
477+
}
478+
}
479+
}
480+
481+
StringBuilder message = new StringBuilder("Elasticsearch exception [");
482+
message.append(TYPE).append('=').append(type).append(", ");
483+
message.append(REASON).append('=').append(reason);
484+
if (stack != null) {
485+
message.append(", ").append(STACK_TRACE).append('=').append(stack);
486+
}
487+
message.append(']');
488+
489+
ElasticsearchException e = new ElasticsearchException(message.toString(), cause);
490+
491+
for (Map.Entry<String, List<String>> entry : metadata.entrySet()) {
492+
//subclasses can print out additional metadata through the metadataToXContent method. Simple key-value pairs will be
493+
//parsed back and become part of this metadata set, while objects and arrays are not supported when parsing back.
494+
//Those key-value pairs become part of the metadata set and inherit the "es." prefix as that is currently required
495+
//by addMetadata. The prefix will get stripped out when printing metadata out so it will be effectively invisible.
496+
//TODO move subclasses that print out simple metadata to using addMetadata directly and support also numbers and booleans.
497+
//TODO rename metadataToXContent and have only SearchPhaseExecutionException use it, which prints out complex objects
498+
e.addMetadata("es." + entry.getKey(), entry.getValue());
499+
}
500+
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
501+
e.addHeader(header.getKey(), header.getValue());
502+
}
503+
return e;
504+
}
505+
394506
/**
395507
* Static toXContent helper method that renders {@link org.elasticsearch.ElasticsearchException} or {@link Throwable} instances
396508
* as XContent, delegating the rendering to {@link #toXContent(XContentBuilder, Params)}
397509
* or {@link #innerToXContent(XContentBuilder, Params, Throwable, String, String, Map, Map, Throwable)}.
398510
*
399-
* This method is usually used when the {@link Throwable} is rendered as a part of another XContent object.
511+
* This method is usually used when the {@link Throwable} is rendered as a part of another XContent object, and its result can
512+
* be parsed back using the {@link #fromXContent(XContentParser)} method.
400513
*/
401514
public static void generateThrowableXContent(XContentBuilder builder, Params params, Throwable t) throws IOException {
402515
t = ExceptionsHelper.unwrapCause(t);
@@ -455,71 +568,6 @@ public static void generateFailureXContent(XContentBuilder builder, Params param
455568
builder.endObject();
456569
}
457570

458-
/**
459-
* Generate a {@link ElasticsearchException} from a {@link XContentParser}. This does not
460-
* return the original exception type (ie NodeClosedException for example) but just wraps
461-
* the type, the reason and the cause of the exception. It also recursively parses the
462-
* tree structure of the cause, returning it as a tree structure of {@link ElasticsearchException}
463-
* instances.
464-
*/
465-
public static ElasticsearchException fromXContent(XContentParser parser) throws IOException {
466-
XContentParser.Token token = parser.nextToken();
467-
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
468-
469-
String type = null, reason = null, stack = null;
470-
ElasticsearchException cause = null;
471-
Map<String, List<String>> metadata = new HashMap<>();
472-
Map<String, Object> headers = new HashMap<>();
473-
474-
do {
475-
String currentFieldName = parser.currentName();
476-
token = parser.nextToken();
477-
if (token.isValue()) {
478-
if (TYPE.equals(currentFieldName)) {
479-
type = parser.text();
480-
} else if (REASON.equals(currentFieldName)) {
481-
reason = parser.text();
482-
} else if (STACK_TRACE.equals(currentFieldName)) {
483-
stack = parser.text();
484-
} else {
485-
metadata.put(currentFieldName, Collections.singletonList(parser.text()));
486-
}
487-
} else if (token == XContentParser.Token.START_OBJECT) {
488-
if (CAUSED_BY.equals(currentFieldName)) {
489-
cause = fromXContent(parser);
490-
} else if (HEADER.equals(currentFieldName)) {
491-
headers.putAll(parser.map());
492-
} else {
493-
throwUnknownField(currentFieldName, parser.getTokenLocation());
494-
}
495-
}
496-
} while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME);
497-
498-
StringBuilder message = new StringBuilder("Elasticsearch exception [");
499-
message.append(TYPE).append('=').append(type).append(", ");
500-
message.append(REASON).append('=').append(reason);
501-
if (stack != null) {
502-
message.append(", ").append(STACK_TRACE).append('=').append(stack);
503-
}
504-
message.append(']');
505-
506-
ElasticsearchException e = new ElasticsearchException(message.toString(), cause);
507-
508-
for (Map.Entry<String, List<String>> entry : metadata.entrySet()) {
509-
//subclasses can print out additional metadata through the metadataToXContent method. Simple key-value pairs will be
510-
//parsed back and become part of this metadata set, while objects and arrays are not supported when parsing back.
511-
//Those key-value pairs become part of the metadata set and inherit the "es." prefix as that is currently required
512-
//by addMetadata. The prefix will get stripped out when printing metadata out so it will be effectively invisible.
513-
//TODO move subclasses that print out simple metadata to using addMetadata directly and support also numbers and booleans.
514-
//TODO rename metadataToXContent and have only SearchPhaseExecutionException use it, which prints out complex objects
515-
e.addMetadata("es." + entry.getKey(), entry.getValue());
516-
}
517-
for (Map.Entry<String, Object> header : headers.entrySet()) {
518-
e.addHeader(header.getKey(), String.valueOf(header.getValue()));
519-
}
520-
return e;
521-
}
522-
523571
/**
524572
* Returns the root cause of this exception or multiple if different shards caused different exceptions
525573
*/

0 commit comments

Comments
 (0)