Add a dev module to run Gitiles from an embedded Jetty server

The various ways of running WARs from maven directly have proven to be
a headache and not work reliably on seemingly-identical environments.
Instead, reduce the amount of Maven magic to a minimum, and use
Jetty's nice embedding support to start a server serving only a
GitilesServlet from /*.

Change-Id: Ib07c5f94cb76e7cd0208096591e3e7cc6148f1ac
diff --git a/gitiles-dev/pom.xml b/gitiles-dev/pom.xml
new file mode 100644
index 0000000..060e5be
--- /dev/null
+++ b/gitiles-dev/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright 2012 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gitiles</groupId>
+    <artifactId>gitiles-parent</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gitiles-dev</artifactId>
+  <packaging>jar</packaging>
+  <name>Gitiles - Development</name>
+
+  <description>
+    Gitiles running with an embedded Jetty server.
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gitiles</groupId>
+      <artifactId>gitiles-servlet</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>${jettyVersion}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <version>${jettyVersion}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>1.7.1</version>
+        <configuration>
+          <transformers>
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+              <mainClass>com.google.gitiles.dev.Main</mainClass>
+            </transformer>
+          </transformers>
+          <createDependencyReducedPom>false</createDependencyReducedPom>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java b/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java
new file mode 100644
index 0000000..7ce5a83
--- /dev/null
+++ b/gitiles-dev/src/main/java/com/google/gitiles/dev/DevServer.java
@@ -0,0 +1,153 @@
+// Copyright 2012 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.gitiles.dev;
+
+import com.google.gitiles.GitilesServlet;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.resource.FileResource;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+class DevServer {
+  private final Server httpd;
+
+  DevServer(Config cfg) throws IOException {
+    httpd = new Server();
+    httpd.setConnectors(connectors(cfg));
+    httpd.setThreadPool(threadPool(cfg));
+    httpd.setHandler(handler());
+  }
+
+  void start() throws Exception {
+    httpd.start();
+    httpd.join();
+  }
+
+  private Connector[] connectors(Config cfg) {
+    Connector c = new SelectChannelConnector();
+    c.setHost(null);
+    c.setPort(cfg.getInt("gitiles", null, "port", 8080));
+    c.setStatsOn(false);
+    return new Connector[]{c};
+  }
+
+  private ThreadPool threadPool(Config cfg) {
+    QueuedThreadPool pool = new QueuedThreadPool();
+    pool.setName("HTTP");
+    pool.setMinThreads(2);
+    pool.setMaxThreads(10);
+    pool.setMaxQueued(50);
+    return pool;
+  }
+
+  private Handler handler() throws IOException {
+    ContextHandlerCollection handlers = new ContextHandlerCollection();
+    handlers.addHandler(staticHandler());
+    handlers.addHandler(appHandler());
+    return handlers;
+
+  }
+
+  private Handler appHandler() {
+    ServletContextHandler handler = new ServletContextHandler();
+    handler.setContextPath("");
+    handler.addServlet(new ServletHolder(new GitilesServlet()), "/*");
+    return handler;
+  }
+
+  private FileNotFoundException badWebRoot(URL u) {
+    return new FileNotFoundException("Cannot find web root from " + u);
+  }
+
+  private FileNotFoundException badWebRoot(URL u, Throwable cause) {
+    FileNotFoundException notFound = badWebRoot(u);
+    notFound.initCause(cause);
+    return notFound;
+  }
+
+  private Handler staticHandler(URL targetUrl) throws IOException {
+    if (!"file".equals(targetUrl.getProtocol())) {
+      throw badWebRoot(targetUrl);
+    }
+    String targetPath = targetUrl.getPath();
+    // targetPath is an arbitrary path under gitiles-dev/target in the standard
+    // Maven package layout.
+    int targetIndex = targetPath.lastIndexOf("gitiles-dev/target/");
+    if (targetIndex < 0) {
+      throw badWebRoot(targetUrl);
+    }
+    String staticPath = targetPath.substring(0, targetIndex)
+        + "./gitiles-servlet/src/main/resources/com/google/gitiles/static";
+    URI staticUri;
+    try {
+      staticUri = new URI("file", staticPath, null).normalize();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
+    File staticRoot = new File(staticUri);
+    if (!staticRoot.exists() || !staticRoot.isDirectory()) {
+      throw badWebRoot(targetUrl);
+    }
+    ResourceHandler rh = new ResourceHandler();
+    try {
+      rh.setBaseResource(new FileResource(staticUri.toURL()));
+    } catch (URISyntaxException e) {
+      throw badWebRoot(targetUrl, e);
+    }
+    rh.setWelcomeFiles(new String[]{});
+    rh.setDirectoriesListed(false);
+    ContextHandler handler = new ContextHandler("/+static");
+    handler.setHandler(rh);
+    return handler;
+  }
+
+  private Handler staticHandler() throws IOException {
+    URL u = getClass().getResource(getClass().getSimpleName() + ".class");
+    if (u == null) {
+      throw new FileNotFoundException("Cannot find web root");
+    }
+    if ("jar".equals(u.getProtocol())) {
+      int jarEntry = u.getPath().indexOf("!/");
+      if (jarEntry < 0) {
+        throw badWebRoot(u);
+      }
+      try {
+        return staticHandler(new URL(u.getPath().substring(0, jarEntry)));
+      } catch (MalformedURLException e) {
+        throw badWebRoot(u, e);
+      }
+    } else {
+      return staticHandler(u);
+    }
+  }
+}
diff --git a/gitiles-dev/src/main/java/com/google/gitiles/dev/Main.java b/gitiles-dev/src/main/java/com/google/gitiles/dev/Main.java
new file mode 100644
index 0000000..204a229
--- /dev/null
+++ b/gitiles-dev/src/main/java/com/google/gitiles/dev/Main.java
@@ -0,0 +1,23 @@
+// Copyright 2012 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.gitiles.dev;
+
+import com.google.gitiles.GitilesConfig;
+
+public class Main {
+  public static void main(String[] args) throws Exception {
+    new DevServer(GitilesConfig.loadDefault()).start();
+  }
+}
diff --git a/pom.xml b/pom.xml
index 4c0b85e..049c528 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,7 @@
   </properties>
 
   <modules>
+    <module>gitiles-dev</module>
     <module>gitiles-servlet</module>
     <module>gitiles-war</module>
   </modules>