diff --git a/core/src/main/java/org/elasticsearch/ElasticsearchException.java b/core/src/main/java/org/elasticsearch/ElasticsearchException.java index 36fa0a7bdfe..99d76313338 100644 --- a/core/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/core/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -51,6 +51,7 @@ import static java.util.Collections.singletonMap; import static java.util.Collections.unmodifiableMap; import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_UUID_NA_VALUE; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName; /** * A base class for all elasticsearch exceptions. @@ -478,16 +479,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte } } - StringBuilder message = new StringBuilder("Elasticsearch exception ["); - message.append(TYPE).append('=').append(type).append(", "); - message.append(REASON).append('=').append(reason); - if (stack != null) { - message.append(", ").append(STACK_TRACE).append('=').append(stack); - } - message.append(']'); - - ElasticsearchException e = new ElasticsearchException(message.toString(), cause); - + ElasticsearchException e = new ElasticsearchException(buildMessage(type, reason, stack), cause); for (Map.Entry> entry : metadata.entrySet()) { //subclasses can print out additional metadata through the metadataToXContent method. Simple key-value pairs will be //parsed back and become part of this metadata set, while objects and arrays are not supported when parsing back. @@ -527,7 +519,8 @@ public class ElasticsearchException extends RuntimeException implements ToXConte * exception is rendered. When it's true all detail are provided including guesses root causes, cause and potentially stack * trace. * - * This method is usually used when the {@link Exception} is rendered as a full XContent object. + * This method is usually used when the {@link Exception} is rendered as a full XContent object, and its output can be parsed + * by the {@link #failureFromXContent(XContentParser)} method. */ public static void generateFailureXContent(XContentBuilder builder, Params params, @Nullable Exception e, boolean detailed) throws IOException { @@ -568,6 +561,27 @@ public class ElasticsearchException extends RuntimeException implements ToXConte builder.endObject(); } + /** + * Parses the output of {@link #generateFailureXContent(XContentBuilder, Params, Exception, boolean)} + */ + public static ElasticsearchException failureFromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + ensureFieldName(parser, token, ERROR); + + token = parser.nextToken(); + if (token.isValue()) { + return new ElasticsearchException(buildMessage("exception", parser.text(), null)); + } + + ensureExpectedToken(token, XContentParser.Token.START_OBJECT, parser::getTokenLocation); + token = parser.nextToken(); + + // TODO Root causes are ignored for now. They will be skipped by innerFromXContent() because + // it ignores metadata arrays of objects. If we decide to parse root causes, we'll have to + // change innerFromXContent() so that it does not parse root causes on its own. + return innerFromXContent(parser); + } + /** * Returns the root cause of this exception or multiple if different shards caused different exceptions */ @@ -613,6 +627,17 @@ public class ElasticsearchException extends RuntimeException implements ToXConte return toUnderscoreCase(simpleName); } + static String buildMessage(String type, String reason, String stack) { + StringBuilder message = new StringBuilder("Elasticsearch exception ["); + message.append(TYPE).append('=').append(type).append(", "); + message.append(REASON).append('=').append(reason); + if (stack != null) { + message.append(", ").append(STACK_TRACE).append('=').append(stack); + } + message.append(']'); + return message.toString(); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java b/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java index 71ef7710e1c..9fba12affa7 100644 --- a/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java +++ b/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java @@ -589,22 +589,67 @@ public class ElasticsearchExceptionTests extends ESTestCase { assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); assertNull(parser.nextToken()); } - assertNotNull(parsedException); + assertDeepEquals(exceptions.v2(), parsedException); + } + + public void testUnknownFailureToAndFromXContent() throws IOException { + final XContent xContent = randomFrom(XContentType.values()).xContent(); + + BytesReference failureBytes = XContentHelper.toXContent((builder, params) -> { + // Prints a null failure using generateFailureXContent() + ElasticsearchException.generateFailureXContent(builder, params, null, randomBoolean()); + return builder; + }, xContent.type(), randomBoolean()); + + ElasticsearchException parsedFailure; + try (XContentParser parser = createParser(xContent, failureBytes)) { + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + parsedFailure = ElasticsearchException.failureFromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + + // Failure was null, expecting a "unknown" reason + assertEquals("Elasticsearch exception [type=exception, reason=unknown]", parsedFailure.getMessage()); + assertEquals(0, parsedFailure.getHeaders().size()); + assertEquals(0, parsedFailure.getMetadata().size()); + } + + public void testFailureToAndFromXContent() throws IOException { + final XContent xContent = randomFrom(XContentType.values()).xContent(); + final Tuple exceptions = randomExceptions(); + final boolean detailed = randomBoolean(); + + Exception failure = (Exception) exceptions.v1(); + BytesReference failureBytes = XContentHelper.toXContent((builder, params) -> { + ElasticsearchException.generateFailureXContent(builder, params, failure, detailed); + return builder; + }, xContent.type(), randomBoolean()); + + ElasticsearchException parsedFailure; + try (XContentParser parser = createParser(xContent, failureBytes)) { + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + parsedFailure = ElasticsearchException.failureFromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + assertNotNull(parsedFailure); ElasticsearchException expected = exceptions.v2(); - do { - assertEquals(expected.getMessage(), parsedException.getMessage()); - assertEquals(expected.getHeaders(), parsedException.getHeaders()); - assertEquals(expected.getMetadata(), parsedException.getMetadata()); - assertEquals(expected.getResourceType(), parsedException.getResourceType()); - assertEquals(expected.getResourceId(), parsedException.getResourceId()); - - expected = (ElasticsearchException) expected.getCause(); - parsedException = (ElasticsearchException) parsedException.getCause(); - if (expected == null) { - assertNull(parsedException); + if (detailed) { + assertDeepEquals(expected, parsedFailure); + } else { + String reason; + if (failure instanceof ElasticsearchException) { + reason = failure.getClass().getSimpleName() + "[" + failure.getMessage() + "]"; + } else { + reason = "No ElasticsearchException found"; } - } while (expected != null); + assertEquals(ElasticsearchException.buildMessage("exception", reason, null), parsedFailure.getMessage()); + assertEquals(0, parsedFailure.getHeaders().size()); + assertEquals(0, parsedFailure.getMetadata().size()); + assertNull(parsedFailure.getCause()); + } } /** @@ -625,6 +670,28 @@ public class ElasticsearchExceptionTests extends ESTestCase { }, expectedJson); } + private static void assertDeepEquals(ElasticsearchException expected, ElasticsearchException actual) { + if (expected == null) { + assertNull(actual); + } else { + assertNotNull(actual); + } + + do { + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getHeaders(), actual.getHeaders()); + assertEquals(expected.getMetadata(), actual.getMetadata()); + assertEquals(expected.getResourceType(), actual.getResourceType()); + assertEquals(expected.getResourceId(), actual.getResourceId()); + + expected = (ElasticsearchException) expected.getCause(); + actual = (ElasticsearchException) actual.getCause(); + if (expected == null) { + assertNull(actual); + } + } while (expected != null); + } + private static Tuple randomExceptions() { Throwable actual; ElasticsearchException expected;