Rewrote struts2 plugin and added some bugfixes to the servlet regex dispatcher.
Struts2 plugin now works with Guice Servlet properly (removing the cycle problem) and doesn't do any malarkey like catching throwables and System.exit(1)ing.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1038 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/servlet/servlet.iml b/servlet/servlet.iml
index 32b98a5..61ff701 100644
--- a/servlet/servlet.iml
+++ b/servlet/servlet.iml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <module relativePaths="true" type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_5" inherit-compiler-output="true">
     <exclude-output />
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
diff --git a/servlet/src/com/google/inject/servlet/UriPatternType.java b/servlet/src/com/google/inject/servlet/UriPatternType.java
index ac59a0d..5449691 100755
--- a/servlet/src/com/google/inject/servlet/UriPatternType.java
+++ b/servlet/src/com/google/inject/servlet/UriPatternType.java
@@ -95,7 +95,6 @@
 
   /**
    * Matchers URIs using a regular expression.
-   * NOTE(dhanji): No path info is available when using regex mapping.
    *
    * @author dhanji@gmail.com (Dhanji R. Prasanna)
    */
@@ -113,7 +112,15 @@
     public String extractPath(String path) {
       Matcher matcher = pattern.matcher(path);
       if (matcher.matches() && matcher.groupCount() >= 1) {
-         return path.substring(0, matcher.start(1));
+
+        // Try to capture the everything before the regex begins to match
+        // the path. This is a rough approximation to try and get parity
+        // with the servlet style mapping where the path is a capture of
+        // the URI before the wildcard.
+        int end = matcher.start(1);
+        if (end < path.length()) {
+          return path.substring(0, end);
+        }
       }
       return null;
     }
diff --git a/struts2/example/root/WEB-INF/Counter.jsp b/struts2/example/root/WEB-INF/Counter.jsp
deleted file mode 100644
index 01f3580..0000000
--- a/struts2/example/root/WEB-INF/Counter.jsp
+++ /dev/null
@@ -1,15 +0,0 @@
-<%@ taglib prefix="s" uri="/struts-tags" %>
-
-<html>
-  <body>
-    <h1>Counter Example</h1>
-    <h3><b>Hits in this session:</b>
-      <s:property value="count"/></h3>
-
-    <h3><b>Status:</b>
-      <s:property value="status"/></h3>
-
-    <h3><b>Message:</b>
-      <s:property value="message"/></h3>
-  </body>
-</html>
\ No newline at end of file
diff --git a/struts2/example/root/WEB-INF/classes/struts.xml b/struts2/example/root/WEB-INF/classes/struts.xml
index 3634d8b..de434c3 100644
--- a/struts2/example/root/WEB-INF/classes/struts.xml
+++ b/struts2/example/root/WEB-INF/classes/struts.xml
@@ -4,13 +4,13 @@
 
 <struts>
 
-  <constant name="guice.module"
-            value="com.google.inject.struts2.example.ExampleModule"/>
+  <!-- No need to specify a module here. See ExampleListenerAndModule. -->
 
+  <!-- Register some actions, these get injected for you by Guice -->
   <package name="default" extends="struts-default">
-    <action name="Count"
+    <action name="Count" 
         class="com.google.inject.struts2.example.Count">
-      <result>/WEB-INF/Counter.jsp</result>
+      <result>/Counter.jsp</result>
     </action>      
   </package>
 
diff --git a/struts2/example/root/WEB-INF/web.xml b/struts2/example/root/WEB-INF/web.xml
index b2d53fd..5878242 100644
--- a/struts2/example/root/WEB-INF/web.xml
+++ b/struts2/example/root/WEB-INF/web.xml
@@ -3,25 +3,19 @@
   "http://java.sun.com/dtd/web-app_2_3.dtd">
 
 <web-app>
-  
+
+  <listener>
+      <listener-class>com.google.inject.struts2.example.ExampleListenerAndModule</listener-class>
+  </listener>  
+
   <filter>
     <filter-name>guice</filter-name>
     <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
   </filter>
 
-  <filter>
-    <filter-name>struts2</filter-name>
-    <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
-  </filter>
-
   <filter-mapping>
     <filter-name>guice</filter-name>
     <url-pattern>/*</url-pattern>
   </filter-mapping>
 
-  <filter-mapping>
-    <filter-name>struts2</filter-name>
-    <url-pattern>/*</url-pattern>
-  </filter-mapping>
-
 </web-app>
diff --git a/struts2/example/src/com/google/inject/struts2/example/ExampleListenerAndModule.java b/struts2/example/src/com/google/inject/struts2/example/ExampleListenerAndModule.java
new file mode 100644
index 0000000..4fc5cac
--- /dev/null
+++ b/struts2/example/src/com/google/inject/struts2/example/ExampleListenerAndModule.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (C) 2006 Google Inc.
+ *
+ * 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.inject.struts2.example;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.GuiceServletContextListener;
+import com.google.inject.servlet.ServletModule;
+import org.apache.struts2.dispatcher.FilterDispatcher;
+
+/**
+ * Example application module.
+ *
+ * @author crazybob@google.com (Bob Lee)
+ */
+public class ExampleListenerAndModule extends GuiceServletContextListener {
+
+  protected Injector getInjector() {
+    return Guice.createInjector(new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        bind(Service.class).to(ServiceImpl.class);
+
+        bind(FilterDispatcher.class).in(Singleton.class);
+        filter("/*").through(org.apache.struts2.dispatcher.FilterDispatcher.class);
+      }
+    });
+  }
+}
diff --git a/struts2/example/src/com/google/inject/struts2/example/ExampleModule.java b/struts2/example/src/com/google/inject/struts2/example/ExampleModule.java
deleted file mode 100644
index 840aa96..0000000
--- a/struts2/example/src/com/google/inject/struts2/example/ExampleModule.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (C) 2006 Google Inc.
- *
- * 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.inject.struts2.example;
-
-import com.google.inject.AbstractModule;
-
-/**
- * Example application module.
- *
- * @author crazybob@google.com (Bob Lee)
- */
-public class ExampleModule extends AbstractModule {
-
-  protected void configure() {
-    bind(Service.class).to(ServiceImpl.class);
-  }
-}
diff --git a/struts2/plugin/src/com/google/inject/servlet/Struts2Factory.java b/struts2/plugin/src/com/google/inject/servlet/Struts2Factory.java
new file mode 100644
index 0000000..295f3c5
--- /dev/null
+++ b/struts2/plugin/src/com/google/inject/servlet/Struts2Factory.java
@@ -0,0 +1,251 @@
+/**
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inject.servlet;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.ScopeAnnotation;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.entities.InterceptorConfig;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.interceptor.Interceptor;
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * Cleanup up version from Bob's GuiceObjectFactory. Now works properly with
+ * GS2 and fixes several bugs.
+ *
+ * @author dhanji@gmail.com
+ */
+public class Struts2Factory extends ObjectFactory {
+
+  static final Logger logger =
+      Logger.getLogger(Struts2Factory.class.getName());
+
+  Module module;
+  volatile Injector strutsInjector;
+  boolean developmentMode = false;
+  List<ProvidedInterceptor> interceptors
+      = new ArrayList<ProvidedInterceptor>();
+  private static final String ERROR_NO_INJECTOR =
+      "Cannot find a Guice injector in the servlet context. Are you"
+          + " sure you registered GuiceServletContextListener in your application's web.xml?";
+
+  @Override
+  public boolean isNoArgConstructorRequired() {
+    return false;
+  }
+
+  @Inject(value = "guice.module", required = false)
+  void setModule(String moduleClassName) {
+    try {
+      // Instantiate user's module.
+      @SuppressWarnings({"unchecked"})
+      Class<? extends Module> moduleClass =
+          (Class<? extends Module>) Class.forName(moduleClassName);
+      this.module = moduleClass.newInstance();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Inject(value = "struts.devMode", required = false)
+  void setDevelopmentMode(String developmentMode) {
+    this.developmentMode = "true".equals(developmentMode.trim());
+  }
+
+  Set<Class<?>> boundClasses = new HashSet<Class<?>>();
+
+  public Class getClassInstance(String name) throws ClassNotFoundException {
+    Class<?> clazz = super.getClassInstance(name);
+
+    synchronized (this) {
+      if (strutsInjector == null) {
+        // We can only bind each class once.
+        if (!boundClasses.contains(clazz)) {
+          try {
+            // Calling these methods now helps us detect ClassNotFoundErrors
+            // early.
+            clazz.getDeclaredFields();
+            clazz.getDeclaredMethods();
+
+            boundClasses.add(clazz);
+          } catch (Throwable t) {
+            // Struts should still work even though some classes aren't in the
+            // classpath. It appears we always get the exception here when
+            // this is the case.
+            return clazz;
+          }
+        }
+      }
+    }
+
+    return clazz;
+  }
+
+  @SuppressWarnings("unchecked")
+  public Object buildBean(Class clazz, Map extraContext) {
+    if (strutsInjector == null) {
+      synchronized (this) {
+        if (strutsInjector == null) {
+          createInjector();
+        }
+      }
+    }
+
+    return strutsInjector.getInstance(clazz);
+  }
+
+  private void createInjector() {
+    logger.info("Loading struts2 Guice support...");
+
+    // Attach to parent Guice injector from GS2.
+    Injector injector = (Injector) GuiceFilter.getServletContext()
+        .getAttribute(GuiceServletContextListener.INJECTOR_NAME);
+
+    // Something is wrong, since this should be there if GuiceServletContextListener
+    // was present.
+    if (null == injector) {
+      logger.severe(ERROR_NO_INJECTOR);
+      throw new RuntimeException(ERROR_NO_INJECTOR);
+    }
+
+    if (module != null) {
+      throw new RuntimeException("The struts2 plugin no longer supports specifying a module"
+          + "via the 'guice.module' property in XML."
+          + " Please install your module via a GuiceServletContextListener instead.");
+    }
+
+    this.strutsInjector = injector.createChildInjector(new AbstractModule() {
+      protected void configure() {
+
+        // Tell the injector about all the action classes, etc., so it
+        // can validate them at startup.
+        for (Class<?> boundClass : boundClasses) {
+          // TODO: Set source from Struts XML.
+          bind(boundClass);
+        }
+
+        // Validate the interceptor class.
+        for (ProvidedInterceptor interceptor : interceptors) {
+          interceptor.validate(binder());
+        }
+      }
+    });
+
+    // Inject interceptors.
+    for (ProvidedInterceptor interceptor : interceptors) {
+      interceptor.inject();
+    }
+
+    logger.info("Injector created successfully.");
+  }
+
+  @SuppressWarnings("unchecked")
+  public Interceptor buildInterceptor(InterceptorConfig interceptorConfig,
+      Map interceptorRefParams) throws ConfigurationException {
+    // Ensure the interceptor class is present.
+    Class<? extends Interceptor> interceptorClass;
+    try {
+      interceptorClass = getClassInstance(interceptorConfig.getClassName());
+    } catch (ClassNotFoundException e) {
+      throw new RuntimeException(e);
+    }
+
+    ProvidedInterceptor providedInterceptor = new ProvidedInterceptor(
+        interceptorConfig, interceptorRefParams, interceptorClass);
+    interceptors.add(providedInterceptor);
+    return providedInterceptor;
+  }
+
+  Interceptor superBuildInterceptor(InterceptorConfig interceptorConfig,
+      Map interceptorRefParams) throws ConfigurationException {
+    return super.buildInterceptor(interceptorConfig, interceptorRefParams);
+  }
+
+  class ProvidedInterceptor implements Interceptor {
+
+    final InterceptorConfig config;
+    final Map params;
+    final Class<? extends Interceptor> interceptorClass;
+    Interceptor delegate;
+
+    ProvidedInterceptor(InterceptorConfig config, Map params,
+        Class<? extends Interceptor> interceptorClass) {
+      this.config = config;
+      this.params = params;
+      this.interceptorClass = interceptorClass;
+    }
+
+    void validate(Binder binder) {
+      // TODO: Set source from Struts XML.
+      if (hasScope(interceptorClass)) {
+        binder.addError("Scoping interceptors is not currently supported."
+            + " Please remove the scope annotation from "
+            + interceptorClass.getName() + ".");
+      }
+
+      // Make sure it implements Interceptor.
+      if (!Interceptor.class.isAssignableFrom(interceptorClass)) {
+        binder.addError(interceptorClass.getName() + " must implement "
+          + Interceptor.class.getName() + ".");
+      }
+    }
+
+    void inject() {
+      delegate = superBuildInterceptor(config, params);
+    }
+
+    public void destroy() {
+      if (null != delegate) {
+        delegate.destroy();
+      }
+    }
+
+    public void init() {
+      throw new AssertionError();
+    }
+
+    public String intercept(ActionInvocation invocation) throws Exception {
+      return delegate.intercept(invocation);
+    }
+  }
+
+  /**
+   * Returns true if the given class has a scope annotation.
+   */
+  private static boolean hasScope(
+      Class<? extends Interceptor> interceptorClass) {
+    for (Annotation annotation : interceptorClass.getAnnotations()) {
+      if (annotation.annotationType()
+          .isAnnotationPresent(ScopeAnnotation.class)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/struts2/plugin/src/com/google/inject/struts2/GuiceObjectFactory.java b/struts2/plugin/src/com/google/inject/struts2/GuiceObjectFactory.java
index 5870ecd..5b753f1 100644
--- a/struts2/plugin/src/com/google/inject/struts2/GuiceObjectFactory.java
+++ b/struts2/plugin/src/com/google/inject/struts2/GuiceObjectFactory.java
@@ -37,6 +37,10 @@
 import java.util.Set;
 import java.util.logging.Logger;
 
+/**
+ * @deprecated Use {@link com.google.inject.servlet.Struts2Factory} instead.
+ */
+@Deprecated
 public class GuiceObjectFactory extends ObjectFactory {
 
   static final Logger logger =
@@ -213,7 +217,9 @@
     }
 
     public void destroy() {
-      delegate.destroy();
+      if (null != delegate) {
+        delegate.destroy();
+      }
     }
 
     public void init() {
diff --git a/struts2/plugin/src/struts-plugin.xml b/struts2/plugin/src/struts-plugin.xml
index 6a31756..381a166 100644
--- a/struts2/plugin/src/struts-plugin.xml
+++ b/struts2/plugin/src/struts-plugin.xml
@@ -8,7 +8,7 @@
 
   <bean type="com.opensymphony.xwork2.ObjectFactory" 
         name="guice"
-        class="com.google.inject.struts2.GuiceObjectFactory"/>
+        class="com.google.inject.servlet.Struts2Factory"/>
 
   <!--  Make the Guice object factory the automatic default -->
   <constant name="struts.objectFactory" value="guice" />