Details
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BillingExceptionJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BillingExceptionJson.java
new file mode 100644
index 0000000..8c654e9
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BillingExceptionJson.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.ning.billing.BillingExceptionBase;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+// Doesn't extend JsonBase (no audit logs)
+public class BillingExceptionJson {
+
+ private final String className;
+ private final Integer code;
+ private final String message;
+ private final String causeClassName;
+ private final String causeMessage;
+ private final List<StackTraceElementJson> stackTrace;
+ // TODO add getSuppressed() from 1.7?
+
+ @JsonCreator
+ public BillingExceptionJson(@JsonProperty("className") final String className,
+ @JsonProperty("code") @Nullable final Integer code,
+ @JsonProperty("message") final String message,
+ @JsonProperty("causeClassName") final String causeClassName,
+ @JsonProperty("causeMessage") final String causeMessage,
+ @JsonProperty("stackTrace") final List<StackTraceElementJson> stackTrace) {
+ this.className = className;
+ this.code = code;
+ this.message = message;
+ this.causeClassName = causeClassName;
+ this.causeMessage = causeMessage;
+ this.stackTrace = stackTrace;
+ }
+
+ public BillingExceptionJson(final Exception exception) {
+ this(exception.getClass().getName(),
+ exception instanceof BillingExceptionBase ? ((BillingExceptionBase) exception).getCode() : null,
+ exception.getLocalizedMessage(),
+ exception.getCause() == null ? null : exception.getCause().getClass().getName(),
+ exception.getCause() == null ? null : exception.getCause().getLocalizedMessage(),
+ Lists.<StackTraceElement, StackTraceElementJson>transform(ImmutableList.<StackTraceElement>copyOf(exception.getStackTrace()),
+ new Function<StackTraceElement, StackTraceElementJson>() {
+ @Override
+ public StackTraceElementJson apply(final StackTraceElement input) {
+ return new StackTraceElementJson(input.getClassName(),
+ input.getFileName(),
+ input.getLineNumber(),
+ input.getMethodName(),
+ input.isNativeMethod());
+ }
+ }));
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getCauseClassName() {
+ return causeClassName;
+ }
+
+ public String getCauseMessage() {
+ return causeMessage;
+ }
+
+ public List<StackTraceElementJson> getStackTrace() {
+ return stackTrace;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BillingExceptionJson{");
+ sb.append("className='").append(className).append('\'');
+ sb.append(", code=").append(code);
+ sb.append(", message='").append(message).append('\'');
+ sb.append(", causeClassName='").append(causeClassName).append('\'');
+ sb.append(", causeMessage='").append(causeMessage).append('\'');
+ sb.append(", stackTrace='").append(stackTrace).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BillingExceptionJson that = (BillingExceptionJson) o;
+
+ if (causeClassName != null ? !causeClassName.equals(that.causeClassName) : that.causeClassName != null) {
+ return false;
+ }
+ if (causeMessage != null ? !causeMessage.equals(that.causeMessage) : that.causeMessage != null) {
+ return false;
+ }
+ if (className != null ? !className.equals(that.className) : that.className != null) {
+ return false;
+ }
+ if (code != null ? !code.equals(that.code) : that.code != null) {
+ return false;
+ }
+ if (message != null ? !message.equals(that.message) : that.message != null) {
+ return false;
+ }
+ if (stackTrace != null ? !stackTrace.equals(that.stackTrace) : that.stackTrace != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = className != null ? className.hashCode() : 0;
+ result = 31 * result + (code != null ? code.hashCode() : 0);
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ result = 31 * result + (causeClassName != null ? causeClassName.hashCode() : 0);
+ result = 31 * result + (causeMessage != null ? causeMessage.hashCode() : 0);
+ result = 31 * result + (stackTrace != null ? stackTrace.hashCode() : 0);
+ return result;
+ }
+
+ public static final class StackTraceElementJson {
+
+ private final String className;
+ private final String fileName;
+ private final Integer lineNumber;
+ private final String methodName;
+ private final Boolean nativeMethod;
+
+ @JsonCreator
+ public StackTraceElementJson(@JsonProperty("className") final String className,
+ @JsonProperty("fileName") final String fileName,
+ @JsonProperty("lineNumber") final Integer lineNumber,
+ @JsonProperty("methodName") final String methodName,
+ @JsonProperty("nativeMethod") final Boolean nativeMethod) {
+ this.className = className;
+ this.fileName = fileName;
+ this.lineNumber = lineNumber;
+ this.methodName = methodName;
+ this.nativeMethod = nativeMethod;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public Integer getLineNumber() {
+ return lineNumber;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public Boolean getNativeMethod() {
+ return nativeMethod;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("StackTraceElementJson{");
+ sb.append("className='").append(className).append('\'');
+ sb.append(", fileName='").append(fileName).append('\'');
+ sb.append(", lineNumber=").append(lineNumber);
+ sb.append(", methodName='").append(methodName).append('\'');
+ sb.append(", nativeMethod=").append(nativeMethod);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final StackTraceElementJson that = (StackTraceElementJson) o;
+
+ if (className != null ? !className.equals(that.className) : that.className != null) {
+ return false;
+ }
+ if (fileName != null ? !fileName.equals(that.fileName) : that.fileName != null) {
+ return false;
+ }
+ if (lineNumber != null ? !lineNumber.equals(that.lineNumber) : that.lineNumber != null) {
+ return false;
+ }
+ if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) {
+ return false;
+ }
+ if (nativeMethod != null ? !nativeMethod.equals(that.nativeMethod) : that.nativeMethod != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = className != null ? className.hashCode() : 0;
+ result = 31 * result + (fileName != null ? fileName.hashCode() : 0);
+ result = 31 * result + (lineNumber != null ? lineNumber.hashCode() : 0);
+ result = 31 * result + (methodName != null ? methodName.hashCode() : 0);
+ result = 31 * result + (nativeMethod != null ? nativeMethod.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
index 7a61ee8..fd2abb3 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -24,18 +24,23 @@ import javax.ws.rs.core.UriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.ning.billing.jaxrs.json.BillingExceptionJson;
+import com.ning.billing.util.jackson.ObjectMapper;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
public abstract class ExceptionMapperBase {
private static final Logger log = LoggerFactory.getLogger(ExceptionMapperBase.class);
+ private static final ObjectMapper mapper = new ObjectMapper();
protected Response buildConflictingRequestResponse(final Exception e, final UriInfo uriInfo) {
// Log the full stacktrace
log.warn("Conflicting request", e);
- return buildConflictingRequestResponse(e.toString(), uriInfo);
+ return buildConflictingRequestResponse(exceptionToString(e), uriInfo);
}
- protected Response buildConflictingRequestResponse(final String error, final UriInfo uriInfo) {
- log.warn("Conflicting request for {}: {}", uriInfo.getRequestUri(), error);
+ private Response buildConflictingRequestResponse(final String error, final UriInfo uriInfo) {
return Response.status(Status.CONFLICT)
.entity(error)
.type(MediaType.TEXT_PLAIN_TYPE)
@@ -45,11 +50,10 @@ public abstract class ExceptionMapperBase {
protected Response buildNotFoundResponse(final Exception e, final UriInfo uriInfo) {
// Log the full stacktrace
log.info("Not found", e);
- return buildNotFoundResponse(e.toString(), uriInfo);
+ return buildNotFoundResponse(exceptionToString(e), uriInfo);
}
- protected Response buildNotFoundResponse(final String error, final UriInfo uriInfo) {
- log.info("Not found for {}: {}", uriInfo.getRequestUri(), error);
+ private Response buildNotFoundResponse(final String error, final UriInfo uriInfo) {
return Response.status(Status.NOT_FOUND)
.entity(error)
.type(MediaType.TEXT_PLAIN_TYPE)
@@ -59,11 +63,10 @@ public abstract class ExceptionMapperBase {
protected Response buildBadRequestResponse(final Exception e, final UriInfo uriInfo) {
// Log the full stacktrace
log.warn("Bad request", e);
- return buildBadRequestResponse(e.toString(), uriInfo);
+ return buildBadRequestResponse(exceptionToString(e), uriInfo);
}
- protected Response buildBadRequestResponse(final String error, final UriInfo uriInfo) {
- log.warn("Bad request for {}: {}", uriInfo.getRequestUri(), error);
+ private Response buildBadRequestResponse(final String error, final UriInfo uriInfo) {
return Response.status(Status.BAD_REQUEST)
.entity(error)
.type(MediaType.TEXT_PLAIN_TYPE)
@@ -73,14 +76,22 @@ public abstract class ExceptionMapperBase {
protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
// Log the full stacktrace
log.warn("Internal error", e);
- return buildInternalErrorResponse(e.toString(), uriInfo);
+ return buildInternalErrorResponse(exceptionToString(e), uriInfo);
}
- protected Response buildInternalErrorResponse(final String error, final UriInfo uriInfo) {
- log.warn("Internal error for {}: {}", uriInfo.getRequestUri(), error);
+ private Response buildInternalErrorResponse(final String error, final UriInfo uriInfo) {
return Response.status(Status.INTERNAL_SERVER_ERROR)
.entity(error)
.type(MediaType.TEXT_PLAIN_TYPE)
.build();
}
+
+ private String exceptionToString(final Exception e) {
+ try {
+ return mapper.writeValueAsString(new BillingExceptionJson(e));
+ } catch (JsonProcessingException jsonException) {
+ log.warn("Unable to serialize exception", jsonException);
+ }
+ return e.toString();
+ }
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBillingExceptionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBillingExceptionJson.java
new file mode 100644
index 0000000..849c1f4
--- /dev/null
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBillingExceptionJson.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.jaxrs.JaxrsTestSuiteNoDB;
+import com.ning.billing.jaxrs.json.BillingExceptionJson.StackTraceElementJson;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestBillingExceptionJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String className = UUID.randomUUID().toString();
+ final int code = Integer.MIN_VALUE;
+ final String message = UUID.randomUUID().toString();
+ final String causeClassName = UUID.randomUUID().toString();
+ final String causeMessage = UUID.randomUUID().toString();
+
+ final BillingExceptionJson exceptionJson = new BillingExceptionJson(className, code, message, causeClassName, causeMessage, ImmutableList.<StackTraceElementJson>of());
+ Assert.assertEquals(exceptionJson.getClassName(), className);
+ Assert.assertEquals(exceptionJson.getCode(), (Integer) code);
+ Assert.assertEquals(exceptionJson.getMessage(), message);
+ Assert.assertEquals(exceptionJson.getCauseClassName(), causeClassName);
+ Assert.assertEquals(exceptionJson.getCauseMessage(), causeMessage);
+ Assert.assertEquals(exceptionJson.getStackTrace().size(), 0);
+
+ final String asJson = mapper.writeValueAsString(exceptionJson);
+ final BillingExceptionJson fromJson = mapper.readValue(asJson, BillingExceptionJson.class);
+ Assert.assertEquals(fromJson, exceptionJson);
+ }
+
+ @Test(groups = "fast")
+ public void testFromException() throws Exception {
+ final String nil = null;
+ try {
+ nil.toString();
+ Assert.fail();
+ } catch (NullPointerException e) {
+ final BillingExceptionJson exceptionJson = new BillingExceptionJson(e);
+ Assert.assertEquals(exceptionJson.getClassName(), e.getClass().getName());
+ Assert.assertNull(exceptionJson.getCode());
+ Assert.assertNull(exceptionJson.getMessage());
+ Assert.assertNull(exceptionJson.getCauseClassName());
+ Assert.assertNull(exceptionJson.getCauseMessage());
+ Assert.assertFalse(exceptionJson.getStackTrace().isEmpty());
+ Assert.assertEquals(exceptionJson.getStackTrace().get(0).getClassName(), TestBillingExceptionJson.class.getName());
+ Assert.assertEquals(exceptionJson.getStackTrace().get(0).getMethodName(), "testFromException");
+ Assert.assertFalse(exceptionJson.getStackTrace().get(0).getNativeMethod());
+ }
+ }
+}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestExceptions.java b/server/src/test/java/com/ning/billing/jaxrs/TestExceptions.java
new file mode 100644
index 0000000..4ba5fb9
--- /dev/null
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestExceptions.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2013 Ning, Incc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ning.billing.jaxrs;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.ErrorCode;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.jaxrs.json.BillingExceptionJson;
+import com.ning.billing.jaxrs.resources.JaxrsResource;
+import com.ning.http.client.Response;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class TestExceptions extends TestJaxrsBase {
+
+ @Test(groups = "slow")
+ public void testExceptionMapping() throws Exception {
+ // Non-existent account
+ final String uri = JaxrsResource.ACCOUNTS_PATH + "/99999999-b103-42f3-8b6e-dd244f1d0747";
+ final Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.NOT_FOUND.getStatusCode());
+
+ final BillingExceptionJson objFromJson = mapper.readValue(response.getResponseBody(), new TypeReference<BillingExceptionJson>() {});
+ Assert.assertNotNull(objFromJson);
+ Assert.assertEquals(objFromJson.getClassName(), AccountApiException.class.getName());
+ Assert.assertEquals(objFromJson.getCode(), (Integer) ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID.getCode());
+ Assert.assertTrue(objFromJson.getStackTrace().size() > 0);
+ }
+}