From 5807db8c3ecfc7de30e529710b768308d7196e55 Mon Sep 17 00:00:00 2001 From: Bilung Lee Date: Wed, 17 Oct 2012 09:38:15 -0700 Subject: [PATCH] SQOOP-629: Provide better exception handling during server-client communication (Jarek Jarcec Cecho) --- .../apache/sqoop/client/core/ClientError.java | 2 +- .../apache/sqoop/client/request/Request.java | 6 +- .../sqoop/client/shell/DeleteCommand.java | 4 +- .../sqoop/client/shell/SqoopCommand.java | 2 +- .../apache/sqoop/client/shell/SqoopShell.java | 7 ++ .../client/utils/ThrowableDisplayer.java | 97 +++++++++++++++ .../org/apache/sqoop/json/ExceptionInfo.java | 73 ----------- .../org/apache/sqoop/json/ThrowableBean.java | 116 ++++++++++++++++++ .../apache/sqoop/json/TestThrowableBean.java | 49 ++++++++ .../sqoop/connector/ConnectorManager.java | 4 + .../handler/ConnectorRequestHandler.java | 6 + .../sqoop/server/SqoopProtocolServlet.java | 7 +- .../sqoop/server/common/ServerError.java | 3 + 13 files changed, 293 insertions(+), 83 deletions(-) create mode 100644 client/src/main/java/org/apache/sqoop/client/utils/ThrowableDisplayer.java delete mode 100644 common/src/main/java/org/apache/sqoop/json/ExceptionInfo.java create mode 100644 common/src/main/java/org/apache/sqoop/json/ThrowableBean.java create mode 100644 common/src/test/java/org/apache/sqoop/json/TestThrowableBean.java diff --git a/client/src/main/java/org/apache/sqoop/client/core/ClientError.java b/client/src/main/java/org/apache/sqoop/client/core/ClientError.java index 1cf42e47..fd3b97d8 100644 --- a/client/src/main/java/org/apache/sqoop/client/core/ClientError.java +++ b/client/src/main/java/org/apache/sqoop/client/core/ClientError.java @@ -39,7 +39,7 @@ public enum ClientError implements ErrorCode { /** We're not able to get user input */ CLIENT_0005("Can't get user input"), - /** There occured exception on server side **/ + /** There occurred exception on server side **/ CLIENT_0006("Server has returned exception"), ; diff --git a/client/src/main/java/org/apache/sqoop/client/request/Request.java b/client/src/main/java/org/apache/sqoop/client/request/Request.java index 5e381c99..b243dfdf 100644 --- a/client/src/main/java/org/apache/sqoop/client/request/Request.java +++ b/client/src/main/java/org/apache/sqoop/client/request/Request.java @@ -28,7 +28,7 @@ import org.apache.sqoop.client.core.ClientError; import org.apache.sqoop.common.SqoopException; import org.apache.sqoop.common.SqoopProtocolConstants; -import org.apache.sqoop.json.ExceptionInfo; +import org.apache.sqoop.json.ThrowableBean; import org.json.simple.JSONObject; import org.json.simple.JSONValue; @@ -91,13 +91,13 @@ public ClientResponse handle(ClientRequest cr) { if(resp.getHeaders().containsKey( SqoopProtocolConstants.HEADER_SQOOP_INTERNAL_ERROR_CODE)) { - ExceptionInfo ex = new ExceptionInfo(); + ThrowableBean ex = new ThrowableBean(); String responseText = resp.getEntity(String.class); JSONObject json = (JSONObject) JSONValue.parse(responseText); ex.restore(json); - throw new SqoopException(ClientError.CLIENT_0006, ex.toString()); + throw new SqoopException(ClientError.CLIENT_0006, ex.getThrowable()); } } diff --git a/client/src/main/java/org/apache/sqoop/client/shell/DeleteCommand.java b/client/src/main/java/org/apache/sqoop/client/shell/DeleteCommand.java index 1ea773b0..bb09bf3a 100644 --- a/client/src/main/java/org/apache/sqoop/client/shell/DeleteCommand.java +++ b/client/src/main/java/org/apache/sqoop/client/shell/DeleteCommand.java @@ -37,6 +37,7 @@ public DeleteCommand(Shell shell) { "Delete", "info"); } + @Override @SuppressWarnings("unchecked") public Object execute(List args) { if (args.size() == 0) { @@ -59,5 +60,6 @@ public Object execute(List args) { } else { String msg = "Usage: delete " + getUsage(); throw new SqoopException(ClientError.CLIENT_0002, msg); - } } + } + } } diff --git a/client/src/main/java/org/apache/sqoop/client/shell/SqoopCommand.java b/client/src/main/java/org/apache/sqoop/client/shell/SqoopCommand.java index c610fd47..9ae693e5 100644 --- a/client/src/main/java/org/apache/sqoop/client/shell/SqoopCommand.java +++ b/client/src/main/java/org/apache/sqoop/client/shell/SqoopCommand.java @@ -137,4 +137,4 @@ protected void resolveVariables(List arg) { } Collections.copy(arg, temp); } -} \ No newline at end of file +} diff --git a/client/src/main/java/org/apache/sqoop/client/shell/SqoopShell.java b/client/src/main/java/org/apache/sqoop/client/shell/SqoopShell.java index d2b56ba2..c9ac8488 100644 --- a/client/src/main/java/org/apache/sqoop/client/shell/SqoopShell.java +++ b/client/src/main/java/org/apache/sqoop/client/shell/SqoopShell.java @@ -23,6 +23,8 @@ import java.util.HashSet; import java.util.Iterator; +import org.apache.sqoop.client.utils.ThrowableDisplayer; +import org.codehaus.groovy.runtime.MethodClosure; import org.codehaus.groovy.tools.shell.Command; import org.codehaus.groovy.tools.shell.CommandRegistry; import org.codehaus.groovy.tools.shell.Groovysh; @@ -45,6 +47,11 @@ public static void main (String[] args) throws Exception System.setProperty("groovysh.prompt", "sqoop"); Groovysh shell = new Groovysh(); + // Install our error hook (exception handling) + shell.setErrorHook( + new MethodClosure(ThrowableDisplayer.class, "errorHook")); + ThrowableDisplayer.setIo(shell.getIo()); + CommandRegistry registry = shell.getRegistry(); @SuppressWarnings("unchecked") Iterator iterator = registry.iterator(); diff --git a/client/src/main/java/org/apache/sqoop/client/utils/ThrowableDisplayer.java b/client/src/main/java/org/apache/sqoop/client/utils/ThrowableDisplayer.java new file mode 100644 index 00000000..8d6e9b48 --- /dev/null +++ b/client/src/main/java/org/apache/sqoop/client/utils/ThrowableDisplayer.java @@ -0,0 +1,97 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.sqoop.client.utils; + +import org.apache.sqoop.client.core.ClientError; +import org.apache.sqoop.common.SqoopException; +import org.codehaus.groovy.tools.shell.IO; + +/** + * Pretty printing of Throwable objects + */ +public class ThrowableDisplayer { + + /** + * Associated shell IO object. + * + * This objects needs to be set explicitly as some of the methods are called + * by Groovy shell without ability to pass additional arguments. + */ + private static IO io; + + public static void setIo(IO ioObject) { + io = ioObject; + } + + /** + * Error hook installed to Groovy shell. + * + * Will display exception that appeared during executing command. In most + * cases we will simply delegate the call to printing throwable method, + * however in case that we've received ClientError.CLIENT_0006 (server + * exception), we will unwrap server issue and view only that as local + * context shouldn't make any difference. + * + * @param t Throwable to be displayed + */ + public static void errorHook(Throwable t) { + io.out.println("@|red Exception has occurred during processing command |@"); + + // If this is server exception from server + if(t instanceof SqoopException + && ((SqoopException)t).getErrorCode() == ClientError.CLIENT_0006) { + io.out.print("@|red Server has returned exception: |@"); + printThrowable(io, t.getCause()); + } else { + printThrowable(io, t); + } + } + + /** + * Pretty print Throwable instance including stack trace and causes. + * + * @param io IO object to use for generating output + * @param t Throwable to display + */ + protected static void printThrowable(IO io, Throwable t) { + io.out.print("@|red Exception: |@"); + io.out.print(t.getClass().getName()); + io.out.print(" @|red Message: |@"); + io.out.print(t.getMessage()); + io.out.println(); + + io.out.println("Stack trace:"); + for(StackTraceElement e : t.getStackTrace()) { + io.out.print("\t @|bold at |@ "); + io.out.print(e.getClassName()); + io.out.print(" (@|bold " + e.getFileName() + ":" + + e.getLineNumber() + ") |@ "); + io.out.println(); + } + + Throwable cause = t.getCause(); + if(cause != null) { + io.out.print("Caused by: "); + printThrowable(io, cause); + } + } + + private ThrowableDisplayer() { + // Instantiation is prohibited + } +} diff --git a/common/src/main/java/org/apache/sqoop/json/ExceptionInfo.java b/common/src/main/java/org/apache/sqoop/json/ExceptionInfo.java deleted file mode 100644 index 68de4b1b..00000000 --- a/common/src/main/java/org/apache/sqoop/json/ExceptionInfo.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF 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 org.apache.sqoop.json; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import org.json.simple.JSONObject; - -public class ExceptionInfo implements JsonBean { - - public static final String ERROR_CODE = "error-code"; - public static final String ERROR_MESSAGE = "error-message"; - public static final String STACK_TRACE = "stack-trace"; - - private String errorCode; - private String errorMessage; - private String stackTrace; - - // For "extract" - public ExceptionInfo(String code, String message, Exception ex) { - errorCode = code; - errorMessage = message; - - StringWriter writer = new StringWriter(); - ex.printStackTrace(new PrintWriter(writer)); - writer.flush(); - - stackTrace = writer.getBuffer().toString(); - } - - // For "restore" - public ExceptionInfo() { - } - - @SuppressWarnings("unchecked") - @Override - public JSONObject extract() { - JSONObject result = new JSONObject(); - result.put(ERROR_CODE, errorCode); - result.put(ERROR_MESSAGE, errorMessage); - result.put(STACK_TRACE, stackTrace); - - return result; - } - - @Override - public void restore(JSONObject jsonObject) { - errorCode = (String) jsonObject.get(ERROR_CODE); - errorMessage = (String) jsonObject.get(ERROR_MESSAGE); - stackTrace = (String) jsonObject.get(STACK_TRACE); - } - - @Override - public String toString() { - return errorMessage; - } -} diff --git a/common/src/main/java/org/apache/sqoop/json/ThrowableBean.java b/common/src/main/java/org/apache/sqoop/json/ThrowableBean.java new file mode 100644 index 00000000..91914e84 --- /dev/null +++ b/common/src/main/java/org/apache/sqoop/json/ThrowableBean.java @@ -0,0 +1,116 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.sqoop.json; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import java.util.LinkedList; +import java.util.List; + +/** + * Transfer throwable. + * + * TODO(jarcec): After SQOOP-627 will get committed, change the throwable + * creation to same class as was on the server instead of Throwable. + */ +public class ThrowableBean implements JsonBean { + + public static final String MESSAGE = "message"; + public static final String STACK_TRACE = "stack-trace"; + public static final String CLASS = "class"; + public static final String METHOD = "method"; + public static final String FILE = "file"; + public static final String LINE = "line"; + public static final String CAUSE = "cause"; + + private Throwable throwable; + + // For "extract" + public ThrowableBean(Throwable ex) { + throwable = ex; + } + + // For "restore" + public ThrowableBean() { + } + + public Throwable getThrowable() { + return throwable; + } + + @Override + @SuppressWarnings("unchecked") + public JSONObject extract() { + JSONObject result = new JSONObject(); + + result.put(MESSAGE, throwable.getMessage()); + result.put(CLASS, throwable.getClass().getName()); + + JSONArray st = new JSONArray(); + for(StackTraceElement element : throwable.getStackTrace()) { + JSONObject obj = new JSONObject(); + + obj.put(CLASS, element.getClassName()); + obj.put(METHOD, element.getMethodName()); + obj.put(FILE, element.getFileName()); + obj.put(LINE, element.getLineNumber()); + + st.add(obj); + } + + result.put(STACK_TRACE, st); + + Throwable cause = throwable.getCause(); + if(cause != null) { + ThrowableBean causeBean = new ThrowableBean(cause); + result.put(CAUSE, causeBean.extract()); + } + + return result; + } + + @Override + public void restore(JSONObject jsonObject) { + throwable = new Throwable((String) jsonObject.get(MESSAGE)); + + List st = new LinkedList(); + for(Object object : (JSONArray)jsonObject.get(STACK_TRACE)) { + JSONObject json = (JSONObject)object; + StackTraceElement element = new StackTraceElement( + (String)json.get(CLASS), + (String)json.get(METHOD), + (String)json.get(FILE), + ((Long)json.get(LINE)).intValue() + ); + st.add(element); + } + + throwable.setStackTrace(st.toArray(new StackTraceElement[]{})); + + Object cause = jsonObject.get(CAUSE); + if(cause != null) { + JSONObject causeJson = (JSONObject)cause; + + ThrowableBean causeBean = new ThrowableBean(); + causeBean.restore(causeJson); + + throwable.initCause(causeBean.getThrowable()); + } + } +} diff --git a/common/src/test/java/org/apache/sqoop/json/TestThrowableBean.java b/common/src/test/java/org/apache/sqoop/json/TestThrowableBean.java new file mode 100644 index 00000000..19a0a277 --- /dev/null +++ b/common/src/test/java/org/apache/sqoop/json/TestThrowableBean.java @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.sqoop.json; + +import junit.framework.TestCase; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +/** + * + */ +public class TestThrowableBean extends TestCase { + public void testSerialization() { + Throwable ex = new RuntimeException("A"); + ex.initCause(new Exception("B")); + + // Serialize it to JSON object + ThrowableBean bean = new ThrowableBean(ex); + JSONObject json = bean.extract(); + + // "Move" it across network in text form + String string = json.toJSONString(); + + // Retrieved transferred object + JSONObject retrievedJson = (JSONObject) JSONValue.parse(string); + ThrowableBean retrievedBean = new ThrowableBean(); + retrievedBean.restore(retrievedJson); + Throwable retrieved = retrievedBean.getThrowable(); + + assertEquals("A", retrieved.getMessage()); + assertEquals("B", retrieved.getCause().getMessage()); + assertNull(retrieved.getCause().getCause()); + } +} diff --git a/core/src/main/java/org/apache/sqoop/connector/ConnectorManager.java b/core/src/main/java/org/apache/sqoop/connector/ConnectorManager.java index e3bf0e1d..f7228d37 100644 --- a/core/src/main/java/org/apache/sqoop/connector/ConnectorManager.java +++ b/core/src/main/java/org/apache/sqoop/connector/ConnectorManager.java @@ -77,6 +77,10 @@ public static ResourceBundle getResourceBundle(long connectorId, public static MConnector getConnectorMetadata(long connectorId) { ConnectorHandler handler = handlerMap.get(nameMap.get(connectorId)); + if(handler == null) { + return null; + } + return handler.getMetadata(); } diff --git a/server/src/main/java/org/apache/sqoop/handler/ConnectorRequestHandler.java b/server/src/main/java/org/apache/sqoop/handler/ConnectorRequestHandler.java index 81044ce7..fda91fd6 100644 --- a/server/src/main/java/org/apache/sqoop/handler/ConnectorRequestHandler.java +++ b/server/src/main/java/org/apache/sqoop/handler/ConnectorRequestHandler.java @@ -25,6 +25,7 @@ import org.apache.sqoop.model.MConnector; import org.apache.sqoop.server.RequestContext; import org.apache.sqoop.server.RequestHandler; +import org.apache.sqoop.server.common.ServerError; import java.util.LinkedList; import java.util.List; @@ -58,6 +59,11 @@ public JsonBean handleEvent(RequestContext ctx) { } else { Long id = Long.parseLong(cid); + // Check that user is not asking for non existing connector id + if(!ConnectorManager.getConnectoIds().contains(id)) { + throw new SqoopException(ServerError.SERVER_0004, "Invalid id " + id); + } + connectors = new LinkedList(); bundles = new LinkedList(); diff --git a/server/src/main/java/org/apache/sqoop/server/SqoopProtocolServlet.java b/server/src/main/java/org/apache/sqoop/server/SqoopProtocolServlet.java index ece3d936..dc0764e9 100644 --- a/server/src/main/java/org/apache/sqoop/server/SqoopProtocolServlet.java +++ b/server/src/main/java/org/apache/sqoop/server/SqoopProtocolServlet.java @@ -26,7 +26,7 @@ import org.apache.log4j.Logger; import org.apache.sqoop.common.ErrorCode; -import org.apache.sqoop.json.ExceptionInfo; +import org.apache.sqoop.json.ThrowableBean; import org.apache.sqoop.common.SqoopException; import org.apache.sqoop.common.SqoopProtocolConstants; import org.apache.sqoop.common.SqoopResponseCode; @@ -136,11 +136,10 @@ private void sendErrorResponse(RequestContext ctx, Exception ex) SqoopProtocolConstants.HEADER_SQOOP_INTERNAL_ERROR_MESSAGE, ex.getMessage()); - ExceptionInfo exceptionInfo = new ExceptionInfo(ec.getCode(), - ex.getMessage(), ex); + ThrowableBean throwableBean = new ThrowableBean(ex); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.getWriter().write(exceptionInfo.extract().toJSONString()); + response.getWriter().write(throwableBean.extract().toJSONString()); } else { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } diff --git a/server/src/main/java/org/apache/sqoop/server/common/ServerError.java b/server/src/main/java/org/apache/sqoop/server/common/ServerError.java index 0a97d2d1..02d9174b 100644 --- a/server/src/main/java/org/apache/sqoop/server/common/ServerError.java +++ b/server/src/main/java/org/apache/sqoop/server/common/ServerError.java @@ -33,6 +33,9 @@ public enum ServerError implements ErrorCode { /** We've received invalid HTTP request */ SERVER_0003("Invalid HTTP request"), + /** Invalid argument in HTTP request */ + SERVER_0004("Invalid argument in HTTP request"), + ; private final String message;