killbill-uncached

jaxrs: return proper JSON for exceptions This fixes #45. Signed-off-by:

7/22/2013 12:27:13 PM

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);
+    }
+}