[NEMO-304] Fail-fast for mis-configuration in user application (#167)

JIRA: [NEMO-304: Fail-fast for mis-configuration in user application](https://issues.apache.org/jira/projects/NEMO/issues/NEMO-304)

**Major changes:**
- This PR makes JobLauncher check the validity of user main method early, so that we can avoid initializing the whole system if there is a mis-configuration.

**Minor changes to note:**
- 

**Tests for the changes:**
- Added `testNotExistingUserMain` in SparkScala.java to test whether the `InvalidUserMainException` is thrown if the argument of `addUserMain()` is a non-existing class.

**Other comments:**
- 

Closes #167 

Co-authored-by: Taegeon Um <taegeonum@gmail.com>
Co-authored-by: Won Wook SONG <wonook@apache.org>
diff --git a/client/src/main/java/org/apache/nemo/client/JobLauncher.java b/client/src/main/java/org/apache/nemo/client/JobLauncher.java
index 4559bad..9873f16 100644
--- a/client/src/main/java/org/apache/nemo/client/JobLauncher.java
+++ b/client/src/main/java/org/apache/nemo/client/JobLauncher.java
@@ -20,6 +20,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.protobuf.ByteString;
+import org.apache.nemo.common.exception.InvalidUserMainException;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.nemo.common.Util;
 import org.apache.nemo.common.ir.IRDAG;
@@ -121,10 +122,12 @@
    * @throws ClassNotFoundException class not found exception.
    * @throws IOException            IO exception.
    */
-  public static void setup(final String[] args) throws InjectionException, ClassNotFoundException, IOException {
+  public static void setup(final String[] args)
+    throws InjectionException, ClassNotFoundException, IOException, InvalidUserMainException {
     // Get Job and Driver Confs
     LOG.info("Project Root Path: {}", Util.fetchProjectRootPath());
     builtJobConf = getJobConf(args);
+    validateJobConfig(builtJobConf);
 
     // Registers actions for launching the DAG.
     LOG.info("Launching RPC Server");
@@ -207,6 +210,29 @@
   }
 
   /**
+   * Validate the configuration of the application's main method.
+   * @param jobConf Configuration of the application.
+   * @throws InvalidUserMainException when the user main is invalid (e.g., non-existing class/method).
+   */
+  private static void validateJobConfig(final Configuration jobConf) throws InvalidUserMainException {
+    final Injector injector = TANG.newInjector(jobConf);
+    try {
+      final String className = injector.getNamedInstance(JobConf.UserMainClass.class);
+      final Class userCode = Class.forName(className);
+      final Method method = userCode.getMethod("main", String[].class);
+      if (!Modifier.isStatic(method.getModifiers())) {
+        throw new InvalidUserMainException("User Main Method not static");
+      }
+      if (!Modifier.isPublic(userCode.getModifiers())) {
+        throw new InvalidUserMainException("User Main Class not public");
+      }
+
+    } catch (final InjectionException | ClassNotFoundException | NoSuchMethodException e) {
+      throw new InvalidUserMainException(e);
+    }
+  }
+
+  /**
    * Launch application using the application DAG.
    * Notice that we launch the DAG one at a time, as the result of a DAG has to be immediately returned to the
    * Java variable before the application can be resumed.
@@ -294,12 +320,6 @@
     final String[] args = userArgsString.isEmpty() ? EMPTY_USER_ARGS : userArgsString.split(" ");
     final Class userCode = Class.forName(className);
     final Method method = userCode.getMethod("main", String[].class);
-    if (!Modifier.isStatic(method.getModifiers())) {
-      throw new RuntimeException("User Main Method not static");
-    }
-    if (!Modifier.isPublic(userCode.getModifiers())) {
-      throw new RuntimeException("User Main Class not public");
-    }
 
     LOG.info("User program started");
     method.invoke(null, (Object) args);
diff --git a/common/src/main/java/org/apache/nemo/common/exception/InvalidUserMainException.java b/common/src/main/java/org/apache/nemo/common/exception/InvalidUserMainException.java
new file mode 100644
index 0000000..13cbd3c
--- /dev/null
+++ b/common/src/main/java/org/apache/nemo/common/exception/InvalidUserMainException.java
@@ -0,0 +1,42 @@
+/*
+ * 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.nemo.common.exception;
+
+/**
+ * InvalidUserMainException.
+ * Thrown when an application's main class is invalid.
+ */
+public final class InvalidUserMainException extends Exception {
+
+  /**
+   * InvalidUserMainException.
+   * @param cause cause
+   */
+  public InvalidUserMainException(final Throwable cause) {
+    super(cause);
+  }
+
+  /**
+   * InvalidUserMainException.
+   * @param message message
+   */
+  public InvalidUserMainException(final String message) {
+    super(message);
+  }
+}
diff --git a/examples/spark/src/test/java/org/apache/nemo/examples/spark/SparkScala.java b/examples/spark/src/test/java/org/apache/nemo/examples/spark/SparkScala.java
index f0731c3..db77804 100644
--- a/examples/spark/src/test/java/org/apache/nemo/examples/spark/SparkScala.java
+++ b/examples/spark/src/test/java/org/apache/nemo/examples/spark/SparkScala.java
@@ -19,6 +19,7 @@
 package org.apache.nemo.examples.spark;
 
 import org.apache.nemo.client.JobLauncher;
+import org.apache.nemo.common.exception.InvalidUserMainException;
 import org.apache.nemo.common.test.ArgBuilder;
 import org.apache.nemo.common.test.ExampleTestArgs;
 import org.apache.nemo.common.test.ExampleTestUtil;
@@ -126,4 +127,15 @@
       .addOptimizationPolicy(DefaultPolicy.class.getCanonicalName())
       .build());
   }
+
+  @Test(expected = InvalidUserMainException.class)
+  public void testNotExistingUserMain() throws Exception {
+    final String notExistingClassName = "XX";
+     JobLauncher.main(builder
+      .addJobId(SparkALS.class.getSimpleName() + "_test")
+      .addUserMain(notExistingClassName)
+      .addUserArgs("100")
+      .addOptimizationPolicy(DefaultPolicy.class.getCanonicalName())
+      .build());
+  }
 }