Add StartupException and subclasses.

This change adds StartupException, and subclasses InvalidConfigurationException,
and UnsupportedPlatformException. StartupExeceptions can be thrown by the
Adaptor.init() method to immediatly abort Application startup.
StartupExceptions bypass the Application retry with back-off wait recovery
mechanism that will re-attempt to start the adaptor in hopes that a recoverable
error has been corrected. Therefore StartupExceptions should be thrown for
unrecoverable situations, such as running on the wrong platform or a
configuration that could never be valid.

Code Review: http://codereview.appspot.com/99980043
diff --git a/src/com/google/enterprise/adaptor/Application.java b/src/com/google/enterprise/adaptor/Application.java
index 7d0af7c..5050dc7 100644
--- a/src/com/google/enterprise/adaptor/Application.java
+++ b/src/com/google/enterprise/adaptor/Application.java
@@ -87,7 +87,8 @@
    * manual shutdown. A shutdown hook is automatically installed that calls
    * {@code stop()}.
    */
-  public synchronized void start() throws IOException, InterruptedException {
+  public synchronized void start() throws IOException, InterruptedException,
+      StartupException {
     synchronized (this) {
       daemonInit();
 
@@ -122,14 +123,16 @@
   /**
    * Really start. This must be called after a successful {@link #daemonInit}.
    */
-  synchronized void daemonStart() throws IOException, InterruptedException {
+  synchronized void daemonStart() throws IOException, InterruptedException,
+      StartupException {
     AdaptorContext context = gsa.setup(primaryServer, dashboardServer, null);
 
     long sleepDurationMillis = 8000;
     // An hour.
     long maxSleepDurationMillis = 60 * 60 * 1000;
-    // Loop until 1) the adaptor starts successfully, 2) stop() is called, or
-    // 3) Thread.interrupt() is called on this thread (which we don't do).
+    // Loop until 1) the adaptor starts successfully, 2) an unrecoverable
+    // StartupException is thrown, 3) stop() is called, or 4) Thread.interrupt()
+    // is called on this thread (which we don't do).
     // Retrying to start the adaptor is helpful in cases where it needs
     // initialization data from a repository that is temporarily down; if the
     // adaptor is running as a service, we don't want to stop starting simply
@@ -144,6 +147,9 @@
         break;
       } catch (InterruptedException ex) {
         throw ex;
+      } catch (StartupException ex) {        
+        log.log(Level.WARNING, "Failed to initialize adaptor", ex);
+        throw ex;
       } catch (Exception ex) {
         log.log(Level.WARNING, "Failed to initialize adaptor", ex);
         if (shutdownSemaphore.tryAcquire(sleepDurationMillis,
@@ -445,14 +451,35 @@
 
     // Setup providing content.
     try {
-      app.start();
-      log.info("doc content serving started");
+      try {
+        app.start();
+        log.info("doc content serving started");
+      } catch (StartupException e) {
+        throw new RuntimeException("could not start serving", e);
+      } catch (IOException e) {
+        throw new RuntimeException("could not start serving", e);
+      }
     } catch (InterruptedException e) {
+      // Do not call stop - it has already been called.
       throw new RuntimeException("could not start serving", e);
-    } catch (IOException e) {
-      throw new RuntimeException("could not start serving", e);
+    } catch (Throwable t) {
+      // Abnormal termination. Make sure any services that may have been
+      // started are shut down.
+      try {
+        app.stop(3, TimeUnit.SECONDS);
+      } catch (Throwable ignored) {
+        // It was a noble try. Just get out.
+      }
+      // Now rethrow.
+      if (t instanceof RuntimeException) {
+        throw (RuntimeException) t;
+      } else if (t instanceof Error) {
+        throw (Error) t;
+      } else {
+        // Very unlikely to happen.
+        throw new RuntimeException(t);
+      }
     }
-
     return app;
   }
 
diff --git a/src/com/google/enterprise/adaptor/InvalidConfigurationException.java b/src/com/google/enterprise/adaptor/InvalidConfigurationException.java
new file mode 100644
index 0000000..c66798c
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/InvalidConfigurationException.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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.google.enterprise.adaptor;
+
+/**
+ * Thrown for unrecoverable configuration errors. Recoverable configuration
+ * problems (such as server off-line) should throw something other than
+ * StartupException, so that the retry with backoff logic will be used.
+ */
+public class InvalidConfigurationException extends StartupException {
+  /**
+   * Constructs a new InvalidConfigurationException with no message and no root
+   * cause.
+   */
+  public InvalidConfigurationException() {
+    super();
+  }
+
+  /**
+   * Constructs a InvalidConfigurationException with a supplied message
+   * but no root cause.
+   *
+   * @param message the message. Can be retrieved by the {@link #getMessage()}
+   *        method.
+   */
+  public InvalidConfigurationException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a InvalidConfigurationException with message and root cause.
+   *
+   * @param message the message. Can be retrieved by the {@link #getMessage()}
+   *        method.
+   * @param rootCause root failure cause
+   */
+  public InvalidConfigurationException(String message, Throwable rootCause) {
+    super(message, rootCause);
+  }
+}
diff --git a/src/com/google/enterprise/adaptor/StartupException.java b/src/com/google/enterprise/adaptor/StartupException.java
new file mode 100644
index 0000000..6ef7673
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/StartupException.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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.google.enterprise.adaptor;
+
+/**
+ * Thrown for unrecoverable startup errors, such as fatal configuration
+ * errors or running on the wrong platform.  StartupExceptions will bypass
+ * the retry with back-off recovery logic and immediately terminate the
+ * adaptor.
+ */
+public class StartupException extends Exception {
+  /**
+   * Constructs a new StartupException with no message and no root
+   * cause.
+   */
+  public StartupException() {
+    super();
+  }
+
+  /**
+   * Constructs a StartupException with a supplied message but no root
+   * cause.
+   *
+   * @param message the message. Can be retrieved by the {@link #getMessage()}
+   *        method.
+   */
+  public StartupException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a StartupException with message and root cause.
+   *
+   * @param message the message. Can be retrieved by the {@link #getMessage()}
+   *        method.
+   * @param rootCause root failure cause
+   */
+  public StartupException(String message, Throwable rootCause) {
+    super(message, rootCause);
+  }
+}
diff --git a/src/com/google/enterprise/adaptor/UnsupportedPlatformException.java b/src/com/google/enterprise/adaptor/UnsupportedPlatformException.java
new file mode 100644
index 0000000..3a2de56
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/UnsupportedPlatformException.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// 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.google.enterprise.adaptor;
+
+/**
+ * Thrown if the adaptor cannot be run on this OS environment.
+ */
+public class UnsupportedPlatformException extends StartupException {
+  /**
+   * Constructs a new UnsupportedPlatformException with a default message.
+   */
+  public UnsupportedPlatformException() {
+    this(System.getProperty("os.name") +
+         " is not a supported platform for this adaptor.");
+  }
+
+  /**
+   * Constructs a UnsupportedPlatformException with a supplied message.
+   *
+   * @param message the message. Can be retrieved by the {@link #getMessage()}
+   *        method.
+   */
+  public UnsupportedPlatformException(String message) {
+    super(message);
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/ApplicationTest.java b/test/com/google/enterprise/adaptor/ApplicationTest.java
index da84358..871afba 100644
--- a/test/com/google/enterprise/adaptor/ApplicationTest.java
+++ b/test/com/google/enterprise/adaptor/ApplicationTest.java
@@ -227,6 +227,31 @@
   }
 
   @Test
+  public void testFailWithStartupException() throws Exception {
+    class FailStartupAdaptor extends NullAdaptor {
+      @Override
+      public void init(AdaptorContext context) throws Exception {
+        throw new StartupException("Unrecoverable error.");
+      }
+    }
+    FailStartupAdaptor adaptor = new FailStartupAdaptor();
+    app = new Application(adaptor, config);
+
+    // Make sure StartupException bypasses the retry after wait logic.
+    long startTime = System.nanoTime();
+    try {
+      app.start();
+      fail("Expected a StartupException, but got none.");
+    } catch (StartupException expected) {
+      long duration = System.nanoTime() - startTime;
+      final long nanosInAMilli = 1000 * 1000;
+      if (duration > 1000 * nanosInAMilli) {
+        fail("StartupException took a long time: " + duration);
+      }
+    }
+  }
+
+  @Test
   public void testFailOnceInitAdaptor() throws Exception {
     class FailFirstAdaptor extends NullAdaptor {
       private int count = 0;
@@ -262,7 +287,7 @@
     private boolean hasBeenShutdownAtSomePoint;
 
     @Override
-    public void init(AdaptorContext context) {
+    public void init(AdaptorContext context) throws Exception {
       inited = true;
     }