From cb245d7ea1096c424107bffdf90d12b5d82fe96c Mon Sep 17 00:00:00 2001 From: "Henning P. Schmiedehausen" Date: Fri, 13 May 2022 19:43:16 -0700 Subject: [PATCH] Support for jakarta servlet API This is a straight port from the guice-servlet code to support the new jakarta servlet API classes (jakarta.*). --- bom/pom.xml | 5 + extensions/jakarta-servlet/build.properties | 2 + extensions/jakarta-servlet/pom.xml | 49 ++ .../jee/AbstractServletModuleBinding.java | 62 ++ .../src/com/google/inject/servlet/jee/BUILD | 64 +++ .../jee/ContinuingHttpServletRequest.java | 154 +++++ .../servlet/jee/DefaultFilterPipeline.java | 49 ++ .../servlet/jee/FilterChainInvocation.java | 139 +++++ .../inject/servlet/jee/FilterDefinition.java | 187 ++++++ .../inject/servlet/jee/FilterPipeline.java | 45 ++ .../servlet/jee/FiltersModuleBuilder.java | 119 ++++ .../inject/servlet/jee/GuiceFilter.java | 253 ++++++++ .../jee/GuiceServletContextListener.java | 60 ++ .../servlet/jee/InstanceFilterBinding.java | 31 + .../jee/InstanceFilterBindingImpl.java | 50 ++ .../servlet/jee/InstanceServletBinding.java | 31 + .../jee/InstanceServletBindingImpl.java | 50 ++ .../servlet/jee/InternalServletModule.java | 145 +++++ .../servlet/jee/LinkedFilterBinding.java | 32 ++ .../servlet/jee/LinkedFilterBindingImpl.java | 53 ++ .../servlet/jee/LinkedServletBinding.java | 32 ++ .../servlet/jee/LinkedServletBindingImpl.java | 53 ++ .../servlet/jee/ManagedFilterPipeline.java | 170 ++++++ .../servlet/jee/ManagedServletPipeline.java | 218 +++++++ .../inject/servlet/jee/RequestParameters.java | 35 ++ .../inject/servlet/jee/RequestScoped.java | 33 ++ .../inject/servlet/jee/RequestScoper.java | 20 + .../inject/servlet/jee/ScopingException.java | 29 + .../inject/servlet/jee/ScopingOnly.java | 36 ++ .../inject/servlet/jee/ServletDefinition.java | 316 ++++++++++ .../inject/servlet/jee/ServletModule.java | 369 ++++++++++++ .../servlet/jee/ServletModuleBinding.java | 40 ++ .../jee/ServletModuleTargetVisitor.java | 65 +++ .../inject/servlet/jee/ServletScopes.java | 435 ++++++++++++++ .../inject/servlet/jee/ServletUtils.java | 229 ++++++++ .../servlet/jee/ServletsModuleBuilder.java | 128 +++++ .../inject/servlet/jee/SessionScoped.java | 34 ++ .../inject/servlet/jee/UriPatternMatcher.java | 46 ++ .../inject/servlet/jee/UriPatternType.java | 192 +++++++ .../inject/servlet/jee/package-info.java | 24 + .../google/inject/servlet/jee/AllTests.java | 58 ++ .../test/com/google/inject/servlet/jee/BUILD | 48 ++ .../inject/servlet/jee/ContextPathTest.java | 305 ++++++++++ .../jee/ContinuingHttpServletRequestTest.java | 101 ++++ .../jee/ContinuingRequestIntegrationTest.java | 244 ++++++++ .../inject/servlet/jee/DummyFilterImpl.java | 51 ++ .../inject/servlet/jee/DummyServlet.java | 27 + .../google/inject/servlet/jee/EdslTest.java | 105 ++++ .../inject/servlet/jee/ExtensionSpiTest.java | 295 ++++++++++ .../servlet/jee/FilterDefinitionTest.java | 321 +++++++++++ .../jee/FilterDispatchIntegrationTest.java | 449 +++++++++++++++ .../servlet/jee/FilterPipelineTest.java | 121 ++++ .../jee/InjectedFilterPipelineTest.java | 172 ++++++ .../servlet/jee/InvalidScopeBindingTest.java | 102 ++++ .../MultiModuleDispatchIntegrationTest.java | 114 ++++ .../jee/MultipleServletInjectorsTest.java | 113 ++++ .../jee/ScopeRequestIntegrationTest.java | 188 ++++++ .../jee/ServletDefinitionPathsTest.java | 332 +++++++++++ .../servlet/jee/ServletDefinitionTest.java | 126 ++++ .../jee/ServletDispatchIntegrationTest.java | 349 ++++++++++++ .../inject/servlet/jee/ServletModuleTest.java | 125 ++++ .../ServletPipelineRequestDispatcherTest.java | 303 ++++++++++ .../inject/servlet/jee/ServletScopesTest.java | 206 +++++++ .../inject/servlet/jee/ServletSpiVisitor.java | 166 ++++++ .../inject/servlet/jee/ServletTest.java | 539 ++++++++++++++++++ .../inject/servlet/jee/ServletTestUtils.java | 130 +++++ .../inject/servlet/jee/ServletUtilsTest.java | 70 +++ .../jee/TransferRequestIntegrationTest.java | 207 +++++++ .../servlet/jee/UriPatternTypeTest.java | 69 +++ .../VarargsFilterDispatchIntegrationTest.java | 189 ++++++ ...VarargsServletDispatchIntegrationTest.java | 249 ++++++++ extensions/pom.xml | 1 + 72 files changed, 9959 insertions(+) create mode 100644 extensions/jakarta-servlet/build.properties create mode 100644 extensions/jakarta-servlet/pom.xml create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/AbstractServletModuleBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/BUILD create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ContinuingHttpServletRequest.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/DefaultFilterPipeline.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterChainInvocation.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterDefinition.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterPipeline.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FiltersModuleBuilder.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceFilter.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceServletContextListener.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBindingImpl.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBindingImpl.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InternalServletModule.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBindingImpl.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBindingImpl.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedFilterPipeline.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedServletPipeline.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestParameters.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoped.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoper.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingException.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingOnly.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletDefinition.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModule.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleBinding.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleTargetVisitor.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletScopes.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletUtils.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletsModuleBuilder.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/SessionScoped.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternMatcher.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternType.java create mode 100644 extensions/jakarta-servlet/src/com/google/inject/servlet/jee/package-info.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/AllTests.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/BUILD create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContextPathTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingHttpServletRequestTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingRequestIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyFilterImpl.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyServlet.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/EdslTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ExtensionSpiTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDefinitionTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDispatchIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterPipelineTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InjectedFilterPipelineTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InvalidScopeBindingTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultiModuleDispatchIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultipleServletInjectorsTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ScopeRequestIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionPathsTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDispatchIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletModuleTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletPipelineRequestDispatcherTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletScopesTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletSpiVisitor.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTestUtils.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletUtilsTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/TransferRequestIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/UriPatternTypeTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsFilterDispatchIntegrationTest.java create mode 100644 extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsServletDispatchIntegrationTest.java diff --git a/bom/pom.xml b/bom/pom.xml index b7d17df0a6..3f52c69907 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -43,6 +43,11 @@ guice-grapher ${project.version} + + com.google.inject.extensions + guice-jakarta-servlet + ${project.version} + com.google.inject.extensions guice-jmx diff --git a/extensions/jakarta-servlet/build.properties b/extensions/jakarta-servlet/build.properties new file mode 100644 index 0000000000..7467012988 --- /dev/null +++ b/extensions/jakarta-servlet/build.properties @@ -0,0 +1,2 @@ +module=com.google.inject.servlet.jee +fragment=true diff --git a/extensions/jakarta-servlet/pom.xml b/extensions/jakarta-servlet/pom.xml new file mode 100644 index 0000000000..1ac5c9b898 --- /dev/null +++ b/extensions/jakarta-servlet/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + + com.google.inject.extensions + extensions-parent + 5.1.1-SNAPSHOT + + + guice-jakarta-servlet + + Google Guice - Extensions - JEE Servlet + + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + provided + + + jakarta.inject + jakarta.inject-api + + + org.easymock + easymock + 3.1 + test + + + + + + + maven-jar-plugin + + + + com.google.guice.extensions.jee.servlet + + + + + + + diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/AbstractServletModuleBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/AbstractServletModuleBinding.java new file mode 100644 index 0000000000..ec23daffae --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/AbstractServletModuleBinding.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.util.Map; + +/** + * Abstract implementation for all servlet module bindings + * + * @author sameb@google.com (Sam Berlin) + */ +class AbstractServletModuleBinding implements ServletModuleBinding { + + private final Map initParams; + private final T target; + private final UriPatternMatcher patternMatcher; + + AbstractServletModuleBinding( + Map initParams, T target, UriPatternMatcher patternMatcher) { + this.initParams = initParams; + this.target = target; + this.patternMatcher = patternMatcher; + } + + @Override + public Map getInitParams() { + return initParams; + } + + @Override + public String getPattern() { + return patternMatcher.getOriginalPattern(); + } + + protected T getTarget() { + return target; + } + + @Override + public UriPatternType getUriPatternType() { + return patternMatcher.getPatternType(); + } + + @Override + public boolean matchesUri(String uri) { + return patternMatcher.matches(uri); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/BUILD b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/BUILD new file mode 100644 index 0000000000..5ddcdff4f0 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/BUILD @@ -0,0 +1,64 @@ +# Copyright 2022 Google Inc. All rights reserved. +# Author: sameb@google.com (Sam Berlin) +load("@rules_java//java:defs.bzl", "java_library") +load("//:mvn.bzl", "gen_maven_artifact") +load( + "//:build_defs.bzl", + "JAVAC_OPTS", + "POM_VERSION", +) + +package( + default_visibility = ["//:src"], +) + +java_library( + name = "request-scoped-annotation", + srcs = ["RequestScoped.java"], + javacopts = JAVAC_OPTS, + plugins = [ + ], + deps = [ + "//third_party/java/jsr330_inject", + ], +) + +java_library( + name = "servlet", + srcs = glob( + ["*.java"], + exclude = ["RequestScoped.java"], + ), + javacopts = JAVAC_OPTS, + plugins = [ + ], + tags = ["maven_coordinates=com.google.inject.extensions:guice-servlet:" + POM_VERSION], + exports = [":request-scoped-annotation"], + deps = [ + ":request-scoped-annotation", + "//core/src/com/google/inject", + "//third_party/java/guava/base", + "//third_party/java/guava/collect", + "//third_party/java/guava/escape", + "//third_party/java/guava/net", + "//third_party/java/jsr330_inject", + "//third_party/java/servlet/servlet_api", + ], +) + +filegroup( + name = "javadoc-srcs", + srcs = glob(["*.java"]), +) + +gen_maven_artifact( + name = "artifact", + artifact_id = "guice-servlet", + artifact_name = "Google Guice - Extensions - Servlet", + artifact_target = ":servlet", + artifact_target_libs = [ + ":request-scoped-annotation", + ], + is_extension = True, + javadoc_srcs = [":javadoc-srcs"], +) diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ContinuingHttpServletRequest.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ContinuingHttpServletRequest.java new file mode 100644 index 0000000000..5e4bb19be3 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ContinuingHttpServletRequest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.Maps; +import com.google.inject.OutOfScopeException; +import java.io.IOException; +import java.util.Map; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpSession; + +/** + * A wrapper for requests that makes requests immutable, taking a snapshot of the original request. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class ContinuingHttpServletRequest extends HttpServletRequestWrapper { + + // We clear out the attributes as they are mutable and not thread-safe. + private final Map attributes = Maps.newHashMap(); + private final Cookie[] cookies; + + public ContinuingHttpServletRequest(HttpServletRequest request) { + super(request); + + Cookie[] originalCookies = request.getCookies(); + if (originalCookies != null) { + int numberOfCookies = originalCookies.length; + cookies = new Cookie[numberOfCookies]; + for (int i = 0; i < numberOfCookies; i++) { + Cookie originalCookie = originalCookies[i]; + + // Snapshot each cookie + freeze. + // No snapshot is required if this is a snapshot of a snapshot(!) + if (originalCookie instanceof ImmutableCookie) { + cookies[i] = originalCookie; + } else { + cookies[i] = new ImmutableCookie(originalCookie); + } + } + } else { + cookies = null; + } + } + + @Override + public HttpSession getSession() { + throw new OutOfScopeException("Cannot access the session in a continued request"); + } + + @Override + public HttpSession getSession(boolean create) { + throw new UnsupportedOperationException("Cannot access the session in a continued request"); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + throw new UnsupportedOperationException("Cannot access raw request on a continued request"); + } + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Cookie[] getCookies() { + // NOTE(user): Cookies themselves are mutable. However a ContinuingHttpServletRequest + // snapshots the original set of cookies it received and imprisons them in immutable + // form. Unfortunately, the cookie array itself is mutable and there is no way for us + // to avoid this. At worst, however, mutation effects are restricted within the scope + // of a single request. Continued requests are not affected after snapshot time. + return cookies; + } + + private static final class ImmutableCookie extends Cookie { + public ImmutableCookie(Cookie original) { + super(original.getName(), original.getValue()); + + super.setMaxAge(original.getMaxAge()); + super.setPath(original.getPath()); + super.setComment(original.getComment()); + super.setSecure(original.getSecure()); + super.setValue(original.getValue()); + super.setVersion(original.getVersion()); + + if (original.getDomain() != null) { + super.setDomain(original.getDomain()); + } + } + + @Override + public void setComment(String purpose) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setDomain(String pattern) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setMaxAge(int expiry) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setPath(String uri) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setSecure(boolean flag) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setValue(String newValue) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + + @Override + public void setVersion(int v) { + throw new UnsupportedOperationException("Cannot modify cookies on a continued request"); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/DefaultFilterPipeline.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/DefaultFilterPipeline.java new file mode 100644 index 0000000000..04a176e1cb --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/DefaultFilterPipeline.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.io.IOException; +import jakarta.inject.Inject; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * This default pipeline simply dispatches to web.xml's servlet pipeline. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + * @see ManagedFilterPipeline See Also ManagedFilterPipeline. + */ +class DefaultFilterPipeline implements FilterPipeline { + @Inject + DefaultFilterPipeline() {} + + @Override + public void initPipeline(ServletContext context) {} + + @Override + public void destroyPipeline() {} + + @Override + public void dispatch( + ServletRequest request, ServletResponse response, FilterChain proceedingFilterChain) + throws IOException, ServletException { + + proceedingFilterChain.doFilter(request, response); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterChainInvocation.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterChainInvocation.java new file mode 100644 index 0000000000..62eb410dff --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterChainInvocation.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.util.List; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * A Filter chain impl which basically passes itself to the "current" filter and iterates the chain + * on {@code doFilter()}. Modeled on something similar in Apache Tomcat. + * + *

Following this, it attempts to dispatch to guice-servlet's registered servlets using the + * ManagedServletPipeline. + * + *

And the end, it proceeds to the web.xml (default) servlet filter chain, if needed. + * + * @author Dhanji R. Prasanna + * @since 1.0 + */ +class FilterChainInvocation implements FilterChain { + + private static final ImmutableSet SERVLET_INTERNAL_METHODS = + ImmutableSet.of(FilterChainInvocation.class.getName() + ".doFilter"); + + private final FilterDefinition[] filterDefinitions; + private final FilterChain proceedingChain; + private final ManagedServletPipeline servletPipeline; + + //state variable tracks current link in filterchain + private int index = -1; + // whether or not we've caught an exception & cleaned up stack traces + private boolean cleanedStacks = false; + + public FilterChainInvocation( + FilterDefinition[] filterDefinitions, + ManagedServletPipeline servletPipeline, + FilterChain proceedingChain) { + + this.filterDefinitions = filterDefinitions; + this.servletPipeline = servletPipeline; + this.proceedingChain = proceedingChain; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + GuiceFilter.Context previous = GuiceFilter.localContext.get(); + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + HttpServletRequest originalRequest = + (previous != null) ? previous.getOriginalRequest() : request; + GuiceFilter.localContext.set(new GuiceFilter.Context(originalRequest, request, response)); + try { + Filter filter = findNextFilter(request); + if (filter != null) { + // call to the filter, which can either consume the request or + // recurse back into this method. (in which case we will go to find the next filter, + // or dispatch to the servlet if no more filters are left) + filter.doFilter(servletRequest, servletResponse, this); + } else { + //we've reached the end of the filterchain, let's try to dispatch to a servlet + final boolean serviced = servletPipeline.service(servletRequest, servletResponse); + + //dispatch to the normal filter chain only if one of our servlets did not match + if (!serviced) { + proceedingChain.doFilter(servletRequest, servletResponse); + } + } + } catch (Throwable t) { + // Only clean on the first pass through -- one exception deep in a filter + // will propogate upward & hit this catch clause multiple times. We don't + // want to iterate through the stack elements for every filter. + if (!cleanedStacks) { + cleanedStacks = true; + pruneStacktrace(t); + } + Throwables.propagateIfInstanceOf(t, ServletException.class); + Throwables.propagateIfInstanceOf(t, IOException.class); + throw Throwables.propagate(t); + } finally { + GuiceFilter.localContext.set(previous); + } + } + + /** + * Iterates over the remaining filter definitions. Returns the first applicable filter, or null if + * none apply. + */ + private Filter findNextFilter(HttpServletRequest request) { + while (++index < filterDefinitions.length) { + Filter filter = filterDefinitions[index].getFilterIfMatching(request); + if (filter != null) { + return filter; + } + } + return null; + } + + /** + * Removes stacktrace elements related to AOP internal mechanics from the throwable's stack trace + * and any causes it may have. + */ + private void pruneStacktrace(Throwable throwable) { + for (Throwable t = throwable; t != null; t = t.getCause()) { + StackTraceElement[] stackTrace = t.getStackTrace(); + List pruned = Lists.newArrayList(); + for (StackTraceElement element : stackTrace) { + String name = element.getClassName() + "." + element.getMethodName(); + if (!SERVLET_INTERNAL_METHODS.contains(name)) { + pruned.add(element); + } + } + t.setStackTrace(pruned.toArray(new StackTraceElement[pruned.size()])); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterDefinition.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterDefinition.java new file mode 100644 index 0000000000..4855e166d6 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterDefinition.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.Iterators; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Scopes; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.JeeProviderInstanceBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; + +/** + * An internal representation of a filter definition against a particular URI pattern. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class FilterDefinition implements ProviderWithExtensionVisitor { + private final Key filterKey; + private final UriPatternMatcher patternMatcher; + private final Map initParams; + // set only if this was bound to an instance of a Filter. + private final Filter filterInstance; + + // always set after init is called. + private final AtomicReference filter = new AtomicReference<>(); + + public FilterDefinition( + Key filterKey, + UriPatternMatcher patternMatcher, + Map initParams, + Filter filterInstance) { + this.filterKey = filterKey; + this.patternMatcher = patternMatcher; + this.initParams = Collections.unmodifiableMap(new HashMap(initParams)); + this.filterInstance = filterInstance; + } + + @Override + public FilterDefinition get() { + return this; + } + + @Override + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, ProviderInstanceBinding binding) { + if (visitor instanceof ServletModuleTargetVisitor) { + if (filterInstance != null) { + return ((ServletModuleTargetVisitor) visitor) + .visit(new InstanceFilterBindingImpl(initParams, filterInstance, patternMatcher)); + } else { + return ((ServletModuleTargetVisitor) visitor) + .visit(new LinkedFilterBindingImpl(initParams, filterKey, patternMatcher)); + } + } else { + return visitor.visit(binding); + } + } + + @Override + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, JeeProviderInstanceBinding binding) { + if (visitor instanceof ServletModuleTargetVisitor) { + if (filterInstance != null) { + return ((ServletModuleTargetVisitor) visitor) + .visit(new InstanceFilterBindingImpl(initParams, filterInstance, patternMatcher)); + } else { + return ((ServletModuleTargetVisitor) visitor) + .visit(new LinkedFilterBindingImpl(initParams, filterKey, patternMatcher)); + } + } else { + return visitor.visit(binding); + } + } + + private boolean shouldFilter(String uri) { + return uri != null && patternMatcher.matches(uri); + } + + public void init( + final ServletContext servletContext, Injector injector, Set initializedSoFar) + throws ServletException { + + // This absolutely must be a singleton, and so is only initialized once. + if (!Scopes.isSingleton(injector.getBinding(filterKey))) { + throw new ServletException( + "Filters must be bound as singletons. " + + filterKey + + " was not bound in singleton scope."); + } + + Filter filter = injector.getInstance(filterKey); + this.filter.set(filter); + + // Only fire init() if this Singleton filter has not already appeared earlier + // in the filter chain. + if (initializedSoFar.contains(filter)) { + return; + } + + // initialize our filter with the configured context params and servlet context + filter.init( + new FilterConfig() { + @Override + public String getFilterName() { + return filterKey.toString(); + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public String getInitParameter(String s) { + return initParams.get(s); + } + + @Override + public Enumeration getInitParameterNames() { + return Iterators.asEnumeration(initParams.keySet().iterator()); + } + }); + + initializedSoFar.add(filter); + } + + public void destroy(Set destroyedSoFar) { + // filters are always singletons + Filter reference = filter.get(); + + // Do nothing if this Filter was invalid (usually due to not being scoped + // properly), or was already destroyed. According to Servlet Spec: it is + // "out of service", and does not need to be destroyed. + // Also prevent duplicate destroys to the same singleton that may appear + // more than once on the filter chain. + if (null == reference || destroyedSoFar.contains(reference)) { + return; + } + + try { + reference.destroy(); + } finally { + destroyedSoFar.add(reference); + } + } + + public Filter getFilterIfMatching(HttpServletRequest request) { + + final String path = ServletUtils.getContextRelativePath(request); + if (shouldFilter(path)) { + return filter.get(); + } else { + return null; + } + } + + //VisibleForTesting + Filter getFilter() { + return filter.get(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterPipeline.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterPipeline.java new file mode 100644 index 0000000000..d86c9bf570 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FilterPipeline.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.ImplementedBy; +import java.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * An internal dispatcher for guice-servlet registered servlets and filters. By default, we assume a + * Guice 1.0 style servlet module is in play. In other words, we dispatch directly to the web.xml + * pipeline after setting up scopes. + * + *

If on the other hand, {@link ServletModule} is used to register managed servlets and/or + * filters, then a different pipeline is bound instead. Which, after dispatching to Guice-injected + * filters and servlets continues to the web.xml pipeline (if necessary). + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@ImplementedBy(DefaultFilterPipeline.class) +interface FilterPipeline { + void initPipeline(ServletContext context) throws ServletException; + + void destroyPipeline(); + + void dispatch(ServletRequest request, ServletResponse response, FilterChain defaultFilterChain) + throws IOException, ServletException; +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FiltersModuleBuilder.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FiltersModuleBuilder.java new file mode 100644 index 0000000000..b15f74b2b1 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/FiltersModuleBuilder.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.internal.UniqueAnnotations; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jakarta.servlet.Filter; + +/** + * Builds the guice module that binds configured filters, with their wrapper FilterDefinitions. Is + * part of the binding EDSL. All Filters and Servlets are always bound as singletons. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class FiltersModuleBuilder { + + private final Binder binder; + + public FiltersModuleBuilder(Binder binder) { + this.binder = binder; + } + + public ServletModule.FilterKeyBindingBuilder filter(List patterns) { + return new FilterKeyBindingBuilderImpl(parsePatterns(UriPatternType.SERVLET, patterns)); + } + + public ServletModule.FilterKeyBindingBuilder filterRegex(List regexes) { + return new FilterKeyBindingBuilderImpl(parsePatterns(UriPatternType.REGEX, regexes)); + } + + private List parsePatterns(UriPatternType type, List patterns) { + List patternMatchers = new ArrayList<>(); + for (String pattern : patterns) { + UriPatternMatcher matcher = null; + try { + matcher = UriPatternType.get(type, pattern); + } catch (IllegalArgumentException iae) { + binder + .skipSources(ServletModule.class, FiltersModuleBuilder.class) + .addError("%s", iae.getMessage()); + } + if (matcher != null) { + patternMatchers.add(matcher); + } + } + return patternMatchers; + } + + //non-static inner class so it can access state of enclosing module class + class FilterKeyBindingBuilderImpl implements ServletModule.FilterKeyBindingBuilder { + private final List uriPatterns; + + private FilterKeyBindingBuilderImpl(List uriPatterns) { + this.uriPatterns = uriPatterns; + } + + @Override + public void through(Class filterKey) { + through(Key.get(filterKey)); + } + + @Override + public void through(Key filterKey) { + through(filterKey, new HashMap()); + } + + @Override + public void through(Filter filter) { + through(filter, new HashMap()); + } + + @Override + public void through(Class filterKey, Map initParams) { + + // Careful you don't accidentally make this method recursive, thank you IntelliJ IDEA! + through(Key.get(filterKey), initParams); + } + + @Override + public void through(Key filterKey, Map initParams) { + through(filterKey, initParams, null); + } + + private void through( + Key filterKey, Map initParams, Filter filterInstance) { + for (UriPatternMatcher pattern : uriPatterns) { + binder + .bind(FilterDefinition.class) + .annotatedWith(UniqueAnnotations.create()) + .toProvider(new FilterDefinition(filterKey, pattern, initParams, filterInstance)); + } + } + + @Override + public void through(Filter filter, Map initParams) { + Key filterKey = Key.get(Filter.class, UniqueAnnotations.create()); + binder.bind(filterKey).toInstance(filter); + through(filterKey, initParams, filter); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceFilter.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceFilter.java new file mode 100644 index 0000000000..7cc875bf50 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceFilter.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.Throwables; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.internal.Errors; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Apply this filter in web.xml above all other filters (typically), to all requests where you plan + * to use servlet scopes. This is also needed in order to dispatch requests to injectable filters + * and servlets: + * + *

+ *  <filter>
+ *    <filter-name>guiceFilter</filter-name>
+ *    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
+ *  </filter>
+ *
+ *  <filter-mapping>
+ *    <filter-name>guiceFilter</filter-name>
+ *    <url-pattern>/*</url-pattern>
+ *  </filter-mapping>
+ *  
+ * + * This filter must appear before every filter that makes use of Guice injection or servlet scopes + * functionality. Typically, you will only register this filter in web.xml and register any other + * filters (and servlets) using a {@link ServletModule}. + * + * @author crazybob@google.com (Bob Lee) + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class GuiceFilter implements Filter { + static final ThreadLocal localContext = new ThreadLocal<>(); + static volatile FilterPipeline pipeline = new DefaultFilterPipeline(); + + /** We allow both the static and dynamic versions of the pipeline to exist. */ + private final FilterPipeline injectedPipeline; + + /** Used to inject the servlets configured via {@link ServletModule} */ + static volatile WeakReference servletContext = + new WeakReference(null); + + private static final String MULTIPLE_INJECTORS_WARNING = + "Multiple Servlet injectors detected. This is a warning " + + "indicating that you have more than one " + + GuiceFilter.class.getSimpleName() + + " running " + + "in your web application. If this is deliberate, you may safely " + + "ignore this message. If this is NOT deliberate however, " + + "your application may not work as expected."; + + private static final Logger LOGGER = Logger.getLogger(GuiceFilter.class.getName()); + + public GuiceFilter() { + // Use the static FilterPipeline + this(null); + } + + @Inject + GuiceFilter(FilterPipeline filterPipeline) { + injectedPipeline = filterPipeline; + } + + //VisibleForTesting + @Inject + static void setPipeline(FilterPipeline pipeline) { + + // This can happen if you create many injectors and they all have their own + // servlet module. This is legal, caveat a small warning. + if (GuiceFilter.pipeline instanceof ManagedFilterPipeline) { + LOGGER.warning(MULTIPLE_INJECTORS_WARNING); + } + + // We overwrite the default pipeline + GuiceFilter.pipeline = pipeline; + } + + //VisibleForTesting + static void reset() { + pipeline = new DefaultFilterPipeline(); + localContext.remove(); + } + + @Override + public void doFilter( + final ServletRequest servletRequest, + final ServletResponse servletResponse, + final FilterChain filterChain) + throws IOException, ServletException { + + final FilterPipeline filterPipeline = getFilterPipeline(); + + Context previous = GuiceFilter.localContext.get(); + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + HttpServletRequest originalRequest = + (previous != null) ? previous.getOriginalRequest() : request; + try { + RequestScoper.CloseableScope scope = new Context(originalRequest, request, response).open(); + try { + //dispatch across the servlet pipeline, ensuring web.xml's filterchain is honored + filterPipeline.dispatch(servletRequest, servletResponse, filterChain); + } finally { + scope.close(); + } + } catch (IOException e) { + throw e; + } catch (ServletException e) { + throw e; + } catch (Exception e) { + Throwables.propagate(e); + } + } + + static HttpServletRequest getOriginalRequest(Key key) { + return getContext(key).getOriginalRequest(); + } + + static HttpServletRequest getRequest(Key key) { + return getContext(key).getRequest(); + } + + static HttpServletResponse getResponse(Key key) { + return getContext(key).getResponse(); + } + + static ServletContext getServletContext() { + return servletContext.get(); + } + + private static Context getContext(Key key) { + Context context = localContext.get(); + if (context == null) { + throw new OutOfScopeException( + "Cannot access scoped [" + + Errors.convert(key) + + "]. Either we are not currently inside an HTTP Servlet request, or you may" + + " have forgotten to apply " + + GuiceFilter.class.getName() + + " as a servlet filter for this request."); + } + return context; + } + + static class Context implements RequestScoper { + final HttpServletRequest originalRequest; + final HttpServletRequest request; + final HttpServletResponse response; + + // Synchronized to prevent two threads from using the same request + // scope concurrently. + final Lock lock = new ReentrantLock(); + + Context( + HttpServletRequest originalRequest, + HttpServletRequest request, + HttpServletResponse response) { + this.originalRequest = originalRequest; + this.request = request; + this.response = response; + } + + HttpServletRequest getOriginalRequest() { + return originalRequest; + } + + HttpServletRequest getRequest() { + return request; + } + + HttpServletResponse getResponse() { + return response; + } + + @Override + public CloseableScope open() { + lock.lock(); + final Context previous = localContext.get(); + localContext.set(this); + return new CloseableScope() { + @Override + public void close() { + localContext.set(previous); + lock.unlock(); + } + }; + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + final ServletContext servletContext = filterConfig.getServletContext(); + + // Store servlet context in a weakreference, for injection + GuiceFilter.servletContext = new WeakReference<>(servletContext); + + // In the default pipeline, this is a noop. However, if replaced + // by a managed pipeline, a lazy init will be triggered the first time + // dispatch occurs. + FilterPipeline filterPipeline = getFilterPipeline(); + filterPipeline.initPipeline(servletContext); + } + + @Override + public void destroy() { + + try { + // Destroy all registered filters & servlets in that order + FilterPipeline filterPipeline = getFilterPipeline(); + filterPipeline.destroyPipeline(); + + } finally { + reset(); + servletContext.clear(); + } + } + + private FilterPipeline getFilterPipeline() { + // Prefer the injected pipeline, but fall back on the static one for web.xml users. + return (null != injectedPipeline) ? injectedPipeline : pipeline; + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceServletContextListener.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceServletContextListener.java new file mode 100644 index 0000000000..2767e653e8 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/GuiceServletContextListener.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.Injector; +import java.lang.ref.WeakReference; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +/** + * As of Guice 2.0 you can still use (your subclasses of) {@code GuiceServletContextListener} class + * as a logical place to create and configure your injector. This will ensure the injector is + * created when the web application is deployed. + * + * @author Kevin Bourrillion (kevinb@google.com) + * @since 2.0 + */ +public abstract class GuiceServletContextListener implements ServletContextListener { + + static final String INJECTOR_NAME = Injector.class.getName(); + + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + final ServletContext servletContext = servletContextEvent.getServletContext(); + + // Set the Servletcontext early for those people who are using this class. + // NOTE(user): This use of the servletContext is deprecated. + GuiceFilter.servletContext = new WeakReference<>(servletContext); + + Injector injector = getInjector(); + injector + .getInstance(InternalServletModule.BackwardsCompatibleServletContextProvider.class) + .set(servletContext); + servletContext.setAttribute(INJECTOR_NAME, injector); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + ServletContext servletContext = servletContextEvent.getServletContext(); + servletContext.removeAttribute(INJECTOR_NAME); + } + + /** Override this method to create (or otherwise obtain a reference to) your injector. */ + protected abstract Injector getInjector(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBinding.java new file mode 100644 index 0000000000..68fed57c15 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBinding.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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.jee; + +import jakarta.servlet.Filter; + +/** + * A binding to a single instance of a filter. + * + * @author sameb@google.com + * @since 3.0 + */ +public interface InstanceFilterBinding extends ServletModuleBinding { + + /** Returns the filter instance that will be used. */ + Filter getFilterInstance(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBindingImpl.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBindingImpl.java new file mode 100644 index 0000000000..ac22aeecaf --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceFilterBindingImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.MoreObjects; +import java.util.Map; +import jakarta.servlet.Filter; + +/** + * Default implementation of InstanceFilterBinding. + * + * @author sameb@google.com (Sam Berlin) + */ +class InstanceFilterBindingImpl extends AbstractServletModuleBinding + implements InstanceFilterBinding { + + InstanceFilterBindingImpl( + Map initParams, Filter target, UriPatternMatcher patternMatcher) { + super(initParams, target, patternMatcher); + } + + @Override + public Filter getFilterInstance() { + return getTarget(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(InstanceFilterBinding.class) + .add("pattern", getPattern()) + .add("initParams", getInitParams()) + .add("uriPatternType", getUriPatternType()) + .add("filterInstance", getFilterInstance()) + .toString(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBinding.java new file mode 100644 index 0000000000..0b635f3b1c --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBinding.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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.jee; + +import jakarta.servlet.http.HttpServlet; + +/** + * A binding to a single instance of a servlet. + * + * @author sameb@google.com + * @since 3.0 + */ +public interface InstanceServletBinding extends ServletModuleBinding { + + /** Returns the servlet instance that will be used. */ + HttpServlet getServletInstance(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBindingImpl.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBindingImpl.java new file mode 100644 index 0000000000..73f0b5925d --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InstanceServletBindingImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.MoreObjects; +import java.util.Map; +import jakarta.servlet.http.HttpServlet; + +/** + * Default implementation of InstanceServletBinding. + * + * @author sameb@google.com (Sam Berlin) + */ +class InstanceServletBindingImpl extends AbstractServletModuleBinding + implements InstanceServletBinding { + + InstanceServletBindingImpl( + Map initParams, HttpServlet target, UriPatternMatcher patternMatcher) { + super(initParams, target, patternMatcher); + } + + @Override + public HttpServlet getServletInstance() { + return getTarget(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(InstanceServletBinding.class) + .add("pattern", getPattern()) + .add("initParams", getInitParams()) + .add("uriPatternType", getUriPatternType()) + .add("servletInstance", getServletInstance()) + .toString(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InternalServletModule.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InternalServletModule.java new file mode 100644 index 0000000000..9e1603e7e5 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/InternalServletModule.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ServletScopes.REQUEST; +import static com.google.inject.servlet.jee.ServletScopes.SESSION; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.Provides; +import java.util.Map; +import java.util.logging.Logger; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +/** + * This is a left-factoring of all ServletModules installed in the system. In other words, this + * module contains the bindings common to all ServletModules, and is bound exactly once per + * injector. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +final class InternalServletModule extends AbstractModule { + + /** + * Special Provider that tries to obtain an injected servlet context, specific to the current + * injector, failing which, it falls back to the static singleton instance that is available in + * the legacy Guice Servlet. + */ + @Singleton + static class BackwardsCompatibleServletContextProvider implements Provider { + private ServletContext injectedServletContext; + + @Inject + BackwardsCompatibleServletContextProvider() {} + + // This setter is called by the GuiceServletContextListener + void set(ServletContext injectedServletContext) { + this.injectedServletContext = injectedServletContext; + } + + @Override + public ServletContext get() { + if (null != injectedServletContext) { + return injectedServletContext; + } + + Logger.getLogger(InternalServletModule.class.getName()) + .warning( + "You are attempting to use a deprecated API (specifically," + + " attempting to @Inject ServletContext inside an eagerly created" + + " singleton. While we allow this for backwards compatibility, be" + + " warned that this MAY have unexpected behavior if you have more" + + " than one injector (with ServletModule) running in the same JVM." + + " Please consult the Guice documentation at" + + " https://github.com/google/guice/wiki/Servlets for more" + + " information."); + return GuiceFilter.getServletContext(); + } + } + + @Override + protected void configure() { + bindScope(RequestScoped.class, REQUEST); + bindScope(SessionScoped.class, SESSION); + bind(ServletRequest.class).to(HttpServletRequest.class); + bind(ServletResponse.class).to(HttpServletResponse.class); + + // inject the pipeline into GuiceFilter so it can route requests correctly + // Unfortunate staticness... =( + // NOTE(user): This is maintained for legacy purposes. + requestStaticInjection(GuiceFilter.class); + + bind(ManagedFilterPipeline.class); + bind(ManagedServletPipeline.class); + bind(FilterPipeline.class).to(ManagedFilterPipeline.class).asEagerSingleton(); + + bind(ServletContext.class).toJeeProvider(BackwardsCompatibleServletContextProvider.class); + bind(BackwardsCompatibleServletContextProvider.class); + } + + @Provides + @Singleton + @ScopingOnly + GuiceFilter provideScopingOnlyGuiceFilter() { + return new GuiceFilter(new DefaultFilterPipeline()); + } + + @Provides + @RequestScoped + HttpServletRequest provideHttpServletRequest() { + return GuiceFilter.getRequest(Key.get(HttpServletRequest.class)); + } + + @Provides + @RequestScoped + HttpServletResponse provideHttpServletResponse() { + return GuiceFilter.getResponse(Key.get(HttpServletResponse.class)); + } + + @Provides + HttpSession provideHttpSession() { + return GuiceFilter.getRequest(Key.get(HttpSession.class)).getSession(); + } + + @SuppressWarnings("unchecked") // defined by getParameterMap() + @Provides + @RequestScoped + @RequestParameters + Map provideRequestParameters(ServletRequest req) { + return req.getParameterMap(); + } + + @Override + public boolean equals(Object o) { + // Is only ever installed internally, so we don't need to check state. + return o instanceof InternalServletModule; + } + + @Override + public int hashCode() { + return InternalServletModule.class.hashCode(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBinding.java new file mode 100644 index 0000000000..244b9b288d --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBinding.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.Key; +import jakarta.servlet.Filter; + +/** + * A linked binding to a filter. + * + * @author sameb@google.com + * @since 3.0 + */ +public interface LinkedFilterBinding extends ServletModuleBinding { + + /** Returns the key used to lookup the filter instance. */ + Key getLinkedKey(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBindingImpl.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBindingImpl.java new file mode 100644 index 0000000000..e9aae86968 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedFilterBindingImpl.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.MoreObjects; +import com.google.inject.Key; +import java.util.Map; +import jakarta.servlet.Filter; + +/** + * Default implementation of LinkedFilterBinding. + * + * @author sameb@google.com (Sam Berlin) + */ +class LinkedFilterBindingImpl extends AbstractServletModuleBinding> + implements LinkedFilterBinding { + + LinkedFilterBindingImpl( + Map initParams, + Key target, + UriPatternMatcher patternMatcher) { + super(initParams, target, patternMatcher); + } + + @Override + public Key getLinkedKey() { + return getTarget(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(LinkedFilterBinding.class) + .add("pattern", getPattern()) + .add("initParams", getInitParams()) + .add("uriPatternType", getUriPatternType()) + .add("linkedFilterKey", getLinkedKey()) + .toString(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBinding.java new file mode 100644 index 0000000000..bab2a5f3b6 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBinding.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.Key; +import jakarta.servlet.http.HttpServlet; + +/** + * A linked binding to a servlet. + * + * @author sameb@google.com + * @since 3.0 + */ +public interface LinkedServletBinding extends ServletModuleBinding { + + /** Returns the key used to lookup the servlet instance. */ + Key getLinkedKey(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBindingImpl.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBindingImpl.java new file mode 100644 index 0000000000..073f04e88f --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/LinkedServletBindingImpl.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.MoreObjects; +import com.google.inject.Key; +import java.util.Map; +import jakarta.servlet.http.HttpServlet; + +/** + * Default implementation of LinkedServletBinding. + * + * @author sameb@google.com (Sam Berlin) + */ +class LinkedServletBindingImpl extends AbstractServletModuleBinding> + implements LinkedServletBinding { + + LinkedServletBindingImpl( + Map initParams, + Key target, + UriPatternMatcher patternMatcher) { + super(initParams, target, patternMatcher); + } + + @Override + public Key getLinkedKey() { + return getTarget(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(LinkedServletBinding.class) + .add("pattern", getPattern()) + .add("initParams", getInitParams()) + .add("uriPatternType", getUriPatternType()) + .add("linkedServletKey", getLinkedKey()) + .toString(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedFilterPipeline.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedFilterPipeline.java new file mode 100644 index 0000000000..827a3d8aef --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedFilterPipeline.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +/** + * Central routing/dispatch class handles lifecycle of managed filters, and delegates to the servlet + * pipeline. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +class ManagedFilterPipeline implements FilterPipeline { + private final FilterDefinition[] filterDefinitions; + private final ManagedServletPipeline servletPipeline; + private final Provider servletContext; + + //Unfortunately, we need the injector itself in order to create filters + servlets + private final Injector injector; + + //Guards a DCL, so needs to be volatile + private volatile boolean initialized = false; + private static final TypeLiteral FILTER_DEFS = + TypeLiteral.get(FilterDefinition.class); + + @Inject + public ManagedFilterPipeline( + Injector injector, + ManagedServletPipeline servletPipeline, + Provider servletContext) { + this.injector = injector; + this.servletPipeline = servletPipeline; + this.servletContext = servletContext; + + this.filterDefinitions = collectFilterDefinitions(injector); + } + + /** + * Introspects the injector and collects all instances of bound {@code List} + * into a master list. + * + *

We have a guarantee that {@link com.google.inject.Injector#getBindings()} returns a map that + * preserves insertion order in entry-set iterators. + */ + private FilterDefinition[] collectFilterDefinitions(Injector injector) { + List filterDefinitions = Lists.newArrayList(); + for (Binding entry : injector.findBindingsByType(FILTER_DEFS)) { + filterDefinitions.add(entry.getProvider().get()); + } + + // Copy to a fixed-size array for speed of iteration. + return filterDefinitions.toArray(new FilterDefinition[filterDefinitions.size()]); + } + + @Override + public synchronized void initPipeline(ServletContext servletContext) throws ServletException { + + //double-checked lock, prevents duplicate initialization + if (initialized) return; + + // Used to prevent duplicate initialization. + Set initializedSoFar = Sets.newIdentityHashSet(); + + for (FilterDefinition filterDefinition : filterDefinitions) { + filterDefinition.init(servletContext, injector, initializedSoFar); + } + + //next, initialize servlets... + servletPipeline.init(servletContext, injector); + + //everything was ok... + initialized = true; + } + + @Override + public void dispatch( + ServletRequest request, ServletResponse response, FilterChain proceedingFilterChain) + throws IOException, ServletException { + + //lazy init of filter pipeline (OK by the servlet specification). This is needed + //in order for us not to force users to create a GuiceServletContextListener subclass. + if (!initialized) { + initPipeline(servletContext.get()); + } + + //obtain the servlet pipeline to dispatch against + new FilterChainInvocation(filterDefinitions, servletPipeline, proceedingFilterChain) + .doFilter(withDispatcher(request, servletPipeline), response); + } + + /** + * Used to create an proxy that dispatches either to the guice-servlet pipeline or the regular + * pipeline based on uri-path match. This proxy also provides minimal forwarding support. + * + *

We cannot forward from a web.xml Servlet/JSP to a guice-servlet (because the filter pipeline + * is not called again). However, we can wrap requests with our own dispatcher to forward the + * *other* way. web.xml Servlets/JSPs can forward to themselves as per normal. + * + *

This is not a problem cuz we intend for people to migrate from web.xml to guice-servlet, + * incrementally, but not the other way around (which, we should actively discourage). + */ + @SuppressWarnings({"JavaDoc", "deprecation"}) + private ServletRequest withDispatcher( + ServletRequest servletRequest, final ManagedServletPipeline servletPipeline) { + + // don't wrap the request if there are no servlets mapped. This prevents us from inserting our + // wrapper unless it's actually going to be used. This is necessary for compatibility for apps + // that downcast their HttpServletRequests to a concrete implementation. + if (!servletPipeline.hasServletsMapped()) { + return servletRequest; + } + + HttpServletRequest request = (HttpServletRequest) servletRequest; + //noinspection OverlyComplexAnonymousInnerClass + return new HttpServletRequestWrapper(request) { + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + final RequestDispatcher dispatcher = servletPipeline.getRequestDispatcher(path); + + return (null != dispatcher) ? dispatcher : super.getRequestDispatcher(path); + } + }; + } + + @Override + public void destroyPipeline() { + //destroy servlets first + servletPipeline.destroy(); + + //go down chain and destroy all our filters + Set destroyedSoFar = Sets.newIdentityHashSet(); + for (FilterDefinition filterDefinition : filterDefinitions) { + filterDefinition.destroy(destroyedSoFar); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedServletPipeline.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedServletPipeline.java new file mode 100644 index 0000000000..5e06037fa0 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ManagedServletPipeline.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +/** + * A wrapping dispatcher for servlets, in much the same way as {@link ManagedFilterPipeline} is for + * filters. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +@Singleton +class ManagedServletPipeline { + private final ServletDefinition[] servletDefinitions; + private static final TypeLiteral SERVLET_DEFS = + TypeLiteral.get(ServletDefinition.class); + + @Inject + public ManagedServletPipeline(Injector injector) { + this.servletDefinitions = collectServletDefinitions(injector); + } + + boolean hasServletsMapped() { + return servletDefinitions.length > 0; + } + + /** + * Introspects the injector and collects all instances of bound {@code List} + * into a master list. + * + *

We have a guarantee that {@link com.google.inject.Injector#getBindings()} returns a map that + * preserves insertion order in entry-set iterators. + */ + private ServletDefinition[] collectServletDefinitions(Injector injector) { + List servletDefinitions = Lists.newArrayList(); + for (Binding entry : injector.findBindingsByType(SERVLET_DEFS)) { + servletDefinitions.add(entry.getProvider().get()); + } + + // Copy to a fixed size array for speed. + return servletDefinitions.toArray(new ServletDefinition[servletDefinitions.size()]); + } + + public void init(ServletContext servletContext, Injector injector) throws ServletException { + Set initializedSoFar = Sets.newIdentityHashSet(); + + for (ServletDefinition servletDefinition : servletDefinitions) { + servletDefinition.init(servletContext, injector, initializedSoFar); + } + } + + public boolean service(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + + //stop at the first matching servlet and service + for (ServletDefinition servletDefinition : servletDefinitions) { + if (servletDefinition.service(request, response)) { + return true; + } + } + + //there was no match... + return false; + } + + public void destroy() { + Set destroyedSoFar = Sets.newIdentityHashSet(); + for (ServletDefinition servletDefinition : servletDefinitions) { + servletDefinition.destroy(destroyedSoFar); + } + } + + /** + * @return Returns a request dispatcher wrapped with a servlet mapped to the given path or null if + * no mapping was found. + */ + RequestDispatcher getRequestDispatcher(String path) { + final String newRequestUri = path; + + // TODO(user): check servlet spec to see if the following is legal or not. + // Need to strip query string if requested... + + for (final ServletDefinition servletDefinition : servletDefinitions) { + if (servletDefinition.shouldServe(path)) { + return new RequestDispatcher() { + @Override + public void forward(ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + Preconditions.checkState( + !servletResponse.isCommitted(), + "Response has been committed--you can only call forward before" + + " committing the response (hint: don't flush buffers)"); + + // clear buffer before forwarding + servletResponse.resetBuffer(); + + ServletRequest requestToProcess; + if (servletRequest instanceof HttpServletRequest) { + requestToProcess = wrapRequest((HttpServletRequest) servletRequest, newRequestUri); + } else { + // This should never happen, but instead of throwing an exception + // we will allow a happy case pass thru for maximum tolerance to + // legacy (and internal) code. + requestToProcess = servletRequest; + } + + // now dispatch to the servlet + doServiceImpl(servletDefinition, requestToProcess, servletResponse); + } + + @Override + public void include(ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + // route to the target servlet + doServiceImpl(servletDefinition, servletRequest, servletResponse); + } + + private void doServiceImpl( + ServletDefinition servletDefinition, + ServletRequest servletRequest, + ServletResponse servletResponse) + throws ServletException, IOException { + servletRequest.setAttribute(REQUEST_DISPATCHER_REQUEST, Boolean.TRUE); + + try { + servletDefinition.doService(servletRequest, servletResponse); + } finally { + servletRequest.removeAttribute(REQUEST_DISPATCHER_REQUEST); + } + } + }; + } + } + + //otherwise, can't process + return null; + } + + // visible for testing + static HttpServletRequest wrapRequest(HttpServletRequest request, String newUri) { + return new RequestDispatcherRequestWrapper(request, newUri); + } + + /** + * A Marker constant attribute that when present in the request indicates to Guice servlet that + * this request has been generated by a request dispatcher rather than the servlet pipeline. In + * accordance with section 8.4.2 of the Servlet 2.4 specification. + */ + public static final String REQUEST_DISPATCHER_REQUEST = "jakarta.servlet.forward.servlet_path"; + + private static class RequestDispatcherRequestWrapper extends HttpServletRequestWrapper { + private final String newRequestUri; + + public RequestDispatcherRequestWrapper( + HttpServletRequest servletRequest, String newRequestUri) { + super(servletRequest); + this.newRequestUri = newRequestUri; + } + + @Override + public String getRequestURI() { + return newRequestUri; + } + + @Override + public StringBuffer getRequestURL() { + StringBuffer url = new StringBuffer(); + String scheme = getScheme(); + int port = getServerPort(); + + url.append(scheme); + url.append("://"); + url.append(getServerName()); + // port might be -1 in some cases (see java.net.URL.getPort) + if (port > 0 + && (("http".equals(scheme) && (port != 80)) + || ("https".equals(scheme) && (port != 443)))) { + url.append(':'); + url.append(port); + } + url.append(getRequestURI()); + + return (url); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestParameters.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestParameters.java new file mode 100644 index 0000000000..299b9f5d22 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestParameters.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.jee; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Apply this to field or parameters of type {@code Map} when you want the HTTP + * request parameter map to be injected. + * + * @author crazybob@google.com (Bob Lee) + */ +@Retention(RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@BindingAnnotation +public @interface RequestParameters {} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoped.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoped.java new file mode 100644 index 0000000000..941433011e --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoped.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import jakarta.inject.Scope; + +/** + * Apply this to implementation classes when you want one instance per request. + * + * @author crazybob@google.com (Bob Lee) + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Scope +public @interface RequestScoped {} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoper.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoper.java new file mode 100644 index 0000000000..ec4dc930fa --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/RequestScoper.java @@ -0,0 +1,20 @@ +package com.google.inject.servlet.jee; + +import java.io.Closeable; + +/** Object that can be used to apply a request scope to a block of code. */ +public interface RequestScoper { + /** + * Opens up the request scope until the returned object is closed. Implementations should ensure + * (e.g. by blocking) that multiple threads cannot open the same request scope concurrently. It is + * allowable to open the same request scope on the same thread, as long as open/close calls are + * correctly nested. + */ + CloseableScope open(); + + /** Closeable subclass that does not throw any exceptions from close. */ + public interface CloseableScope extends Closeable { + @Override + void close(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingException.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingException.java new file mode 100644 index 0000000000..06310814d4 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 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.jee; + +/** + * Exception thrown when there was a failure entering request scope. + * + * @author Chris Nokleberg + * @since 4.0 + */ +public final class ScopingException extends IllegalStateException { + public ScopingException(String message) { + super(message); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingOnly.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingOnly.java new file mode 100644 index 0000000000..538d1b587d --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ScopingOnly.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 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.jee; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates a {@link GuiceFilter} that provides scope functionality, but doesn't dispatch to {@link + * ServletModule} bound servlets or filters. + * + * @author iqshum@google.com (Isaac Shum) + * @since 4.0 + */ +@Retention(RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@BindingAnnotation +public @interface ScopingOnly {} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletDefinition.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletDefinition.java new file mode 100644 index 0000000000..62e7225bf5 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletDefinition.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST; + +import com.google.common.collect.Iterators; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Scopes; +import com.google.inject.spi.BindingTargetVisitor; +import com.google.inject.spi.JeeProviderInstanceBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; + +/** + * An internal representation of a servlet definition mapped to a particular URI pattern. Also + * performs the request dispatch to that servlet. How nice and OO =) + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +class ServletDefinition implements ProviderWithExtensionVisitor { + private final Key servletKey; + private final UriPatternMatcher patternMatcher; + private final Map initParams; + // set only if this was bound using a servlet instance. + private final HttpServlet servletInstance; + + //always set in init, our servlet is always presumed to be a singleton + private final AtomicReference httpServlet = new AtomicReference<>(); + + public ServletDefinition( + Key servletKey, + UriPatternMatcher patternMatcher, + Map initParams, + HttpServlet servletInstance) { + this.servletKey = servletKey; + this.patternMatcher = patternMatcher; + this.initParams = Collections.unmodifiableMap(new HashMap(initParams)); + this.servletInstance = servletInstance; + } + + @Override + public ServletDefinition get() { + return this; + } + + @Override + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, ProviderInstanceBinding binding) { + if (visitor instanceof ServletModuleTargetVisitor) { + if (servletInstance != null) { + return ((ServletModuleTargetVisitor) visitor) + .visit(new InstanceServletBindingImpl(initParams, servletInstance, patternMatcher)); + } else { + return ((ServletModuleTargetVisitor) visitor) + .visit(new LinkedServletBindingImpl(initParams, servletKey, patternMatcher)); + } + } else { + return visitor.visit(binding); + } + } + + @Override + public V acceptExtensionVisitor( + BindingTargetVisitor visitor, JeeProviderInstanceBinding binding) { + if (visitor instanceof ServletModuleTargetVisitor) { + if (servletInstance != null) { + return ((ServletModuleTargetVisitor) visitor) + .visit(new InstanceServletBindingImpl(initParams, servletInstance, patternMatcher)); + } else { + return ((ServletModuleTargetVisitor) visitor) + .visit(new LinkedServletBindingImpl(initParams, servletKey, patternMatcher)); + } + } else { + return visitor.visit(binding); + } + } + + boolean shouldServe(String uri) { + return uri != null && patternMatcher.matches(uri); + } + + public void init( + final ServletContext servletContext, Injector injector, Set initializedSoFar) + throws ServletException { + + // This absolutely must be a singleton, and so is only initialized once. + if (!Scopes.isSingleton(injector.getBinding(servletKey))) { + throw new ServletException( + "Servlets must be bound as singletons. " + + servletKey + + " was not bound in singleton scope."); + } + + HttpServlet httpServlet = injector.getInstance(servletKey); + this.httpServlet.set(httpServlet); + + // Only fire init() if we have not appeared before in the filter chain. + if (initializedSoFar.contains(httpServlet)) { + return; + } + + // initialize our servlet with the configured context params and servlet context + httpServlet.init( + new ServletConfig() { + @Override + public String getServletName() { + return servletKey.toString(); + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public String getInitParameter(String s) { + return initParams.get(s); + } + + @Override + public Enumeration getInitParameterNames() { + return Iterators.asEnumeration(initParams.keySet().iterator()); + } + }); + + // Mark as initialized. + initializedSoFar.add(httpServlet); + } + + public void destroy(Set destroyedSoFar) { + HttpServlet reference = httpServlet.get(); + + // Do nothing if this Servlet was invalid (usually due to not being scoped + // properly). According to Servlet Spec: it is "out of service", and does not + // need to be destroyed. + // Also prevent duplicate destroys to the same singleton that may appear + // more than once on the filter chain. + if (null == reference || destroyedSoFar.contains(reference)) { + return; + } + + try { + reference.destroy(); + } finally { + destroyedSoFar.add(reference); + } + } + + /** + * Wrapper around the service chain to ensure a servlet is servicing what it must and provides it + * with a wrapped request. + * + * @return Returns true if this servlet triggered for the given request. Or false if guice-servlet + * should continue dispatching down the servlet pipeline. + * @throws IOException If thrown by underlying servlet + * @throws ServletException If thrown by underlying servlet + */ + public boolean service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final String path = ServletUtils.getContextRelativePath(request); + + final boolean serve = shouldServe(path); + + //invocations of the chain end at the first matched servlet + if (serve) { + doService(servletRequest, servletResponse); + } + + //return false if no servlet matched (so we can proceed down to the web.xml servlets) + return serve; + } + + /** + * Utility that delegates to the actual service method of the servlet wrapped with a contextual + * request (i.e. with correctly computed path info). + * + *

We need to suppress deprecation coz we use HttpServletRequestWrapper, which implements + * deprecated API for backwards compatibility. + */ + void doService(final ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + + HttpServletRequest request = + new HttpServletRequestWrapper((HttpServletRequest) servletRequest) { + private boolean pathComputed; + private String path; + + private boolean pathInfoComputed; + private String pathInfo; + + @Override + public String getPathInfo() { + if (!isPathInfoComputed()) { + String servletPath = getServletPath(); + int servletPathLength = servletPath.length(); + String requestUri = getRequestURI(); + pathInfo = requestUri.substring(getContextPath().length()).replaceAll("[/]{2,}", "/"); + // See: https://github.com/google/guice/issues/372 + if (pathInfo.startsWith(servletPath)) { + pathInfo = pathInfo.substring(servletPathLength); + // Corner case: when servlet path & request path match exactly (without trailing '/'), + // then pathinfo is null. + if (pathInfo.isEmpty() && servletPathLength > 0) { + pathInfo = null; + } else { + try { + pathInfo = new URI(pathInfo).getPath(); + } catch (URISyntaxException e) { + // ugh, just leave it alone then + } + } + } else { + pathInfo = null; // we know nothing additional about the URI. + } + pathInfoComputed = true; + } + + return pathInfo; + } + + // NOTE(user): These two are a bit of a hack to help ensure that request dispatcher-sent + // requests don't use the same path info that was memoized for the original request. + // NOTE(user): I don't think this is possible, since the dispatcher-sent request would + // perform its own wrapping. + private boolean isPathInfoComputed() { + return pathInfoComputed + && servletRequest.getAttribute(REQUEST_DISPATCHER_REQUEST) == null; + } + + private boolean isPathComputed() { + return pathComputed && servletRequest.getAttribute(REQUEST_DISPATCHER_REQUEST) == null; + } + + @Override + public String getServletPath() { + return computePath(); + } + + @Override + public String getPathTranslated() { + final String info = getPathInfo(); + + return (null == info) ? null : getRealPath(info); + } + + // Memoizer pattern. + private String computePath() { + if (!isPathComputed()) { + String servletPath = super.getServletPath(); + path = patternMatcher.extractPath(servletPath); + pathComputed = true; + + if (null == path) { + path = servletPath; + } + } + + return path; + } + }; + + doServiceImpl(request, (HttpServletResponse) servletResponse); + } + + private void doServiceImpl(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + GuiceFilter.Context previous = GuiceFilter.localContext.get(); + HttpServletRequest originalRequest = + (previous != null) ? previous.getOriginalRequest() : request; + GuiceFilter.localContext.set(new GuiceFilter.Context(originalRequest, request, response)); + try { + httpServlet.get().service(request, response); + } finally { + GuiceFilter.localContext.set(previous); + } + } + + String getKey() { + return servletKey.toString(); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModule.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModule.java new file mode 100644 index 0000000000..359e1983f2 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModule.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableList; +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import java.util.Map; +import jakarta.servlet.Filter; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServlet; + +/** + * Configures the servlet scopes and creates bindings for the servlet API objects so you can inject + * the request, response, session, etc. + * + *

You should subclass this module to register servlets and filters in the {@link + * #configureServlets()} method. + * + * @author crazybob@google.com (Bob Lee) + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class ServletModule extends AbstractModule { + + @Override + protected final void configure() { + checkState(filtersModuleBuilder == null, "Re-entry is not allowed."); + checkState(servletsModuleBuilder == null, "Re-entry is not allowed."); + filtersModuleBuilder = new FiltersModuleBuilder(binder()); + servletsModuleBuilder = new ServletsModuleBuilder(binder()); + try { + // Install common bindings (skipped if already installed). + install(new InternalServletModule()); + + // Install local filter and servlet bindings. + configureServlets(); + } finally { + filtersModuleBuilder = null; + servletsModuleBuilder = null; + } + } + + /** + * + * + *

Servlet Mapping EDSL

+ * + *

Part of the EDSL builder language for configuring servlets and filters with guice-servlet. + * Think of this as an in-code replacement for web.xml. Filters and servlets are configured here + * using simple java method calls. Here is a typical example of registering a filter when creating + * your Guice injector: + * + *

+   *   Guice.createInjector(..., new ServletModule() {
+   *
+   *     {@literal @}Override
+   *     protected void configureServlets() {
+   *       serve("*.html").with(MyServlet.class)
+   *     }
+   *   }
+   * 
+ * + * This registers a servlet (subclass of {@code HttpServlet}) called {@code MyServlet} to service + * any web pages ending in {@code .html}. You can also use a path-style syntax to register + * servlets: + * + *
+   *       serve("/my/*").with(MyServlet.class)
+   * 
+ * + * Every servlet (or filter) is required to be a singleton. If you cannot annotate the class + * directly, you should add a separate {@code bind(..).in(Singleton.class)} rule elsewhere in your + * module. Mapping a servlet that is bound under any other scope is an error. + * + *

+ * + *

Dispatch Order

+ * + * You are free to register as many servlets and filters as you like this way. They will be + * compared and dispatched in the order in which the filter methods are called: + * + *
+   *
+   *   Guice.createInjector(..., new ServletModule() {
+   *
+   *     {@literal @}Override
+   *     protected void configureServlets() {
+   *       filter("/*").through(MyFilter.class);
+   *       filter("*.css").through(MyCssFilter.class);
+   *       filter("*.jpg").through(new MyJpgFilter());
+   *       // etc..
+   *
+   *       serve("*.html").with(MyServlet.class);
+   *       serve("/my/*").with(MyServlet.class);
+   *       serve("*.jpg").with(new MyServlet());
+   *       // etc..
+   *      }
+   *    }
+   * 
+ * + * This will traverse down the list of rules in lexical order. For example, a url "{@code + * /my/file.js}" (after it runs through the matching filters) will first be compared against the + * servlet mapping: + * + *
+   *       serve("*.html").with(MyServlet.class);
+   * 
+ * + * And failing that, it will descend to the next servlet mapping: + * + *
+   *       serve("/my/*").with(MyServlet.class);
+   * 
+ * + * Since this rule matches, Guice Servlet will dispatch to {@code MyServlet}. These two mapping + * rules can also be written in more compact form using varargs syntax: + * + *
+   *       serve("*.html", "/my/*").with(MyServlet.class);
+   * 
+ * + * This way you can map several URI patterns to the same servlet. A similar syntax is also + * available for filter mappings. + * + *

+ * + *

Regular Expressions

+ * + * You can also map servlets (or filters) to URIs using regular expressions: + * + *
+   *    serveRegex("(.)*ajax(.)*").with(MyAjaxServlet.class)
+   * 
+ * + * This will map any URI containing the text "ajax" in it to {@code MyAjaxServlet}. Such as: + * + *
    + *
  • http://www.google.com/ajax.html + *
  • http://www.google.com/content/ajax/index + *
  • http://www.google.com/it/is_totally_ajaxian + *
+ * + *

Initialization Parameters

+ * + * Servlets (and filters) allow you to pass in init params using the {@code } tag in + * web.xml. You can similarly pass in parameters to Servlets and filters registered in + * Guice-servlet using a {@link java.util.Map} of parameter name/value pairs. For example, to + * initialize {@code MyServlet} with two parameters ({@code name="Dhanji", site="google.com"}) you + * could write: + * + *
+   *  Map<String, String> params = new HashMap<String, String>();
+   *  params.put("name", "Dhanji");
+   *  params.put("site", "google.com");
+   *
+   *  ...
+   *      serve("/*").with(MyServlet.class, params)
+   * 
+ * + *

+ * + *

Binding Keys

+ * + * You can also bind keys rather than classes. This lets you hide implementations with + * package-local visbility and expose them using only a Guice module and an annotation: + * + *
+   *  ...
+   *      filter("/*").through(Key.get(Filter.class, Fave.class));
+   * 
+ * + * Where {@code Filter.class} refers to the Servlet API interface and {@code Fave.class} is a + * custom binding annotation. Elsewhere (in one of your own modules) you can bind this filter's + * implementation: + * + *
+   *   bind(Filter.class).annotatedWith(Fave.class).to(MyFilterImpl.class);
+   * 
+ * + * See {@link com.google.inject.Binder} for more information on binding syntax. + * + *

+ * + *

Multiple Modules

+ * + * It is sometimes useful to capture servlet and filter mappings from multiple different modules. + * This is essential if you want to package and offer drop-in Guice plugins that provide servlet + * functionality. + * + *

Guice Servlet allows you to register several instances of {@code ServletModule} to your + * injector. The order in which these modules are installed determines the dispatch order of + * filters and the precedence order of servlets. For example, if you had two servlet modules, + * {@code RpcModule} and {@code WebServiceModule} and they each contained a filter that mapped to + * the same URI pattern, {@code "/*"}: + * + *

In {@code RpcModule}: + * + *

+   *     filter("/*").through(RpcFilter.class);
+   * 
+ * + * In {@code WebServiceModule}: + * + *
+   *     filter("/*").through(WebServiceFilter.class);
+   * 
+ * + * Then the order in which these filters are dispatched is determined by the order in which the + * modules are installed: + * + *
+   *   install(new WebServiceModule());
+   *   install(new RpcModule());
+   * 
+ * + * In the case shown above {@code WebServiceFilter} will run first. + * + * @since 2.0 + */ + protected void configureServlets() {} + + private FiltersModuleBuilder filtersModuleBuilder; + private ServletsModuleBuilder servletsModuleBuilder; + + private FiltersModuleBuilder getFiltersModuleBuilder() { + checkState( + filtersModuleBuilder != null, "This method can only be used inside configureServlets()"); + return filtersModuleBuilder; + } + + private ServletsModuleBuilder getServletModuleBuilder() { + checkState( + servletsModuleBuilder != null, "This method can only be used inside configureServlets()"); + return servletsModuleBuilder; + } + + /** + * @param urlPattern Any Servlet-style pattern. examples: /*, /html/*, *.html, etc. + * @since 2.0 + */ + protected final FilterKeyBindingBuilder filter(String urlPattern, String... morePatterns) { + return getFiltersModuleBuilder() + .filter(ImmutableList.builder().add(urlPattern).add(morePatterns).build()); + } + + /** + * @param urlPatterns Any Servlet-style patterns. examples: /*, /html/*, *.html, etc. + * @since 4.1 + */ + protected final FilterKeyBindingBuilder filter(Iterable urlPatterns) { + return getFiltersModuleBuilder().filter(ImmutableList.copyOf(urlPatterns)); + } + + /** + * @param regex Any Java-style regular expression. + * @since 2.0 + */ + protected final FilterKeyBindingBuilder filterRegex(String regex, String... regexes) { + return getFiltersModuleBuilder() + .filterRegex(ImmutableList.builder().add(regex).add(regexes).build()); + } + + /** + * @param regexes Any Java-style regular expressions. + * @since 4.1 + */ + protected final FilterKeyBindingBuilder filterRegex(Iterable regexes) { + return getFiltersModuleBuilder().filterRegex(ImmutableList.copyOf(regexes)); + } + + /** + * @param urlPattern Any Servlet-style pattern. examples: /*, /html/*, *.html, etc. + * @since 2.0 + */ + protected final ServletKeyBindingBuilder serve(String urlPattern, String... morePatterns) { + return getServletModuleBuilder() + .serve(ImmutableList.builder().add(urlPattern).add(morePatterns).build()); + } + + /** + * @param urlPatterns Any Servlet-style patterns. examples: /*, /html/*, *.html, etc. + * @since 4.1 + */ + protected final ServletKeyBindingBuilder serve(Iterable urlPatterns) { + return getServletModuleBuilder().serve(ImmutableList.copyOf(urlPatterns)); + } + + /** + * @param regex Any Java-style regular expression. + * @since 2.0 + */ + protected final ServletKeyBindingBuilder serveRegex(String regex, String... regexes) { + return getServletModuleBuilder() + .serveRegex(ImmutableList.builder().add(regex).add(regexes).build()); + } + + /** + * @param regexes Any Java-style regular expressions. + * @since 4.1 + */ + protected final ServletKeyBindingBuilder serveRegex(Iterable regexes) { + return getServletModuleBuilder().serveRegex(ImmutableList.copyOf(regexes)); + } + + /** + * This method only works if you are using the {@linkplain GuiceServletContextListener} to create + * your injector. Otherwise, it returns null. + * + * @return The current servlet context. + * @since 3.0 + */ + protected final ServletContext getServletContext() { + return GuiceFilter.getServletContext(); + } + + /** + * See the EDSL examples at {@link ServletModule#configureServlets()} + * + * @since 2.0 + */ + public static interface FilterKeyBindingBuilder { + void through(Class filterKey); + + void through(Key filterKey); + /** @since 3.0 */ + void through(Filter filter); + + void through(Class filterKey, Map initParams); + + void through(Key filterKey, Map initParams); + /** @since 3.0 */ + void through(Filter filter, Map initParams); + } + + /** + * See the EDSL examples at {@link ServletModule#configureServlets()} + * + * @since 2.0 + */ + public static interface ServletKeyBindingBuilder { + void with(Class servletKey); + + void with(Key servletKey); + /** @since 3.0 */ + void with(HttpServlet servlet); + + void with(Class servletKey, Map initParams); + + void with(Key servletKey, Map initParams); + /** @since 3.0 */ + void with(HttpServlet servlet, Map initParams); + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleBinding.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleBinding.java new file mode 100644 index 0000000000..8f45888038 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleBinding.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.util.Map; + +/** + * A binding created by {@link ServletModule}. + * + * @author sameb@google.com (Sam Berlin) + * @since 3.0 + */ +public interface ServletModuleBinding { + + /** Returns the pattern type that this binding was created with. */ + UriPatternType getUriPatternType(); + + /** Returns the pattern used to match against the binding. */ + String getPattern(); + + /** Returns any context params supplied when creating the binding. */ + Map getInitParams(); + + /** Returns true if the given URI will match this binding. */ + boolean matchesUri(String uri); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleTargetVisitor.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleTargetVisitor.java new file mode 100644 index 0000000000..2faa189d11 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletModuleTargetVisitor.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.servlet.jee.ServletModule.FilterKeyBindingBuilder; +import com.google.inject.servlet.jee.ServletModule.ServletKeyBindingBuilder; +import com.google.inject.spi.BindingTargetVisitor; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServlet; + +/** + * A visitor for the servlet extension. + * + *

If your {@link BindingTargetVisitor} implements this interface, bindings created by using + * {@link ServletModule} will be visited through this interface. + * + * @since 3.0 + * @author sameb@google.com (Sam Berlin) + */ +public interface ServletModuleTargetVisitor extends BindingTargetVisitor { + + /** + * Visits a filter binding created by {@link ServletModule#filter}, where {@link + * FilterKeyBindingBuilder#through} is called with a Class or Key. + * + *

If multiple patterns were specified, this will be called multiple times. + */ + V visit(LinkedFilterBinding binding); + + /** + * Visits a filter binding created by {@link ServletModule#filter} where {@link + * FilterKeyBindingBuilder#through} is called with a {@link Filter}. + * + *

If multiple patterns were specified, this will be called multiple times. + */ + V visit(InstanceFilterBinding binding); + + /** + * Visits a servlet binding created by {@link ServletModule#serve} where {@link + * ServletKeyBindingBuilder#with}, is called with a Class or Key. + * + *

If multiple patterns were specified, this will be called multiple times. + */ + V visit(LinkedServletBinding binding); + + /** + * Visits a servlet binding created by {@link ServletModule#serve} where {@link + * ServletKeyBindingBuilder#with}, is called with an {@link HttpServlet}. + * + *

If multiple patterns were specified, this will be called multiple times. + */ + V visit(InstanceServletBinding binding); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletScopes.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletScopes.java new file mode 100644 index 0000000000..a4fcf9c10b --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletScopes.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +/** + * Servlet scopes. + * + * @author crazybob@google.com (Bob Lee) + */ +public final class ServletScopes { + + private ServletScopes() {} + + /** + * A threadlocal scope map for non-http request scopes. The {@link #REQUEST} scope falls back to + * this scope map if no http request is available, and requires {@link #scopeRequest} to be called + * as an alternative. + */ + private static final ThreadLocal requestScopeContext = new ThreadLocal<>(); + + /** A sentinel attribute value representing null. */ + enum NullObject { + INSTANCE + } + + /** HTTP servlet request scope. */ + public static final Scope REQUEST = new RequestScope(); + + private static final class RequestScope implements Scope { + @Override + public Provider scope(final Key key, final Provider creator) { + return new Provider() { + + /** Keys bound in request-scope which are handled directly by GuiceFilter. */ + private final ImmutableSet> REQUEST_CONTEXT_KEYS = + ImmutableSet.of( + Key.get(HttpServletRequest.class), + Key.get(HttpServletResponse.class), + new Key>(RequestParameters.class) {}); + + @Override + public T get() { + // Check if the alternate request scope should be used, if no HTTP + // request is in progress. + if (null == GuiceFilter.localContext.get()) { + + // NOTE(user): We don't need to synchronize on the scope map + // unlike the HTTP request because we're the only ones who have + // a reference to it, and it is only available via a threadlocal. + Context context = requestScopeContext.get(); + if (null != context) { + @SuppressWarnings("unchecked") + T t = (T) context.map.get(key); + + // Accounts for @Nullable providers. + if (NullObject.INSTANCE == t) { + return null; + } + + if (t == null) { + t = creator.get(); + if (!Scopes.isCircularProxy(t)) { + // Store a sentinel for provider-given null values. + context.map.put(key, t != null ? t : NullObject.INSTANCE); + } + } + + return t; + } // else: fall into normal HTTP request scope and out of scope + // exception is thrown. + } + + // Always synchronize and get/set attributes on the underlying request + // object since Filters may wrap the request and change the value of + // {@code GuiceFilter.getRequest()}. + // + // This _correctly_ throws up if the thread is out of scope. + HttpServletRequest request = GuiceFilter.getOriginalRequest(key); + if (REQUEST_CONTEXT_KEYS.contains(key)) { + // Don't store these keys as attributes, since they are handled by + // GuiceFilter itself. + return creator.get(); + } + String name = key.toString(); + synchronized (request) { + Object obj = request.getAttribute(name); + if (NullObject.INSTANCE == obj) { + return null; + } + @SuppressWarnings("unchecked") + T t = (T) obj; + if (t == null) { + t = creator.get(); + if (!Scopes.isCircularProxy(t)) { + request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); + } + } + return t; + } + } + + @Override + public String toString() { + return String.format("%s[%s]", creator, REQUEST); + } + }; + } + + @Override + public String toString() { + return "ServletScopes.REQUEST"; + } + } + + /** HTTP session scope. */ + public static final Scope SESSION = new SessionScope(); + + private static final class SessionScope implements Scope { + @Override + public Provider scope(final Key key, final Provider creator) { + final String name = key.toString(); + return new Provider() { + @Override + public T get() { + HttpSession session = GuiceFilter.getRequest(key).getSession(); + synchronized (session) { + Object obj = session.getAttribute(name); + if (NullObject.INSTANCE == obj) { + return null; + } + @SuppressWarnings("unchecked") + T t = (T) obj; + if (t == null) { + t = creator.get(); + if (!Scopes.isCircularProxy(t)) { + session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE); + } + } + return t; + } + } + + @Override + public String toString() { + return String.format("%s[%s]", creator, SESSION); + } + }; + } + + @Override + public String toString() { + return "ServletScopes.SESSION"; + } + } + + /** + * Wraps the given callable in a contextual callable that "continues" the HTTP request in another + * thread. This acts as a way of transporting request context data from the request processing + * thread to to worker threads. + * + *

There are some limitations: + * + *

    + *
  • Derived objects (i.e. anything marked @RequestScoped will not be transported. + *
  • State changes to the HttpServletRequest after this method is called will not be seen in the + * continued thread. + *
  • Only the HttpServletRequest, ServletContext and request parameter map are available in the + * continued thread. The response and session are not available. + *
+ * + *

The returned callable will throw a {@link ScopingException} when called if the HTTP request + * scope is still active on the current thread. + * + * @param callable code to be executed in another thread, which depends on the request scope. + * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To + * seed a key with null, use {@code null} as the value. + * @return a callable that will invoke the given callable, making the request context available to + * it. + * @throws OutOfScopeException if this method is called from a non-request thread, or if the + * request has completed. + * @since 3.0 + * @deprecated You probably want to use {@code transferRequest} instead + */ + @Deprecated + public static Callable continueRequest(Callable callable, Map, Object> seedMap) { + return wrap(callable, continueRequest(seedMap)); + } + + private static RequestScoper continueRequest(Map, Object> seedMap) { + Preconditions.checkArgument( + null != seedMap, "Seed map cannot be null, try passing in Collections.emptyMap() instead."); + + // Snapshot the seed map and add all the instances to our continuing HTTP request. + final ContinuingHttpServletRequest continuingRequest = + new ContinuingHttpServletRequest(GuiceFilter.getRequest(Key.get(HttpServletRequest.class))); + for (Map.Entry, Object> entry : seedMap.entrySet()) { + Object value = validateAndCanonicalizeValue(entry.getKey(), entry.getValue()); + continuingRequest.setAttribute(entry.getKey().toString(), value); + } + + return new RequestScoper() { + @Override + public CloseableScope open() { + checkScopingState( + null == GuiceFilter.localContext.get(), + "Cannot continue request in the same thread as a HTTP request!"); + return new GuiceFilter.Context(continuingRequest, continuingRequest, null).open(); + } + }; + } + + /** + * Wraps the given callable in a contextual callable that "transfers" the request to another + * thread. This acts as a way of transporting request context data from the current thread to a + * future thread. + * + *

As opposed to {@link #continueRequest}, this method propagates all existing scoped objects. + * The primary use case is in server implementations where you can detach the request processing + * thread while waiting for data, and reattach to a different thread to finish processing at a + * later time. + * + *

Because request-scoped objects are not typically thread-safe, the callable returned by this + * method must not be run on a different thread until the current request scope has terminated. + * The returned callable will block until the current thread has released the request scope. + * + * @param callable code to be executed in another thread, which depends on the request scope. + * @return a callable that will invoke the given callable, making the request context available to + * it. + * @throws OutOfScopeException if this method is called from a non-request thread, or if the + * request has completed. + * @since 4.0 + */ + public static Callable transferRequest(Callable callable) { + return wrap(callable, transferRequest()); + } + + /** + * Returns an object that "transfers" the request to another thread. This acts as a way of + * transporting request context data from the current thread to a future thread. The transferred + * scope is the one active for the thread that calls this method. A later call to {@code open()} + * activates the transferred the scope, including propagating any objects scoped at that time. + * + *

As opposed to {@link #continueRequest}, this method propagates all existing scoped objects. + * The primary use case is in server implementations where you can detach the request processing + * thread while waiting for data, and reattach to a different thread to finish processing at a + * later time. + * + *

Because request-scoped objects are not typically thread-safe, it is important to avoid + * applying the same request scope concurrently. The returned Scoper will block on open until the + * current thread has released the request scope. + * + * @return an object that when opened will initiate the request scope + * @throws OutOfScopeException if this method is called from a non-request thread, or if the + * request has completed. + * @since 4.1 + */ + public static RequestScoper transferRequest() { + return (GuiceFilter.localContext.get() != null) + ? transferHttpRequest() + : transferNonHttpRequest(); + } + + private static RequestScoper transferHttpRequest() { + final GuiceFilter.Context context = GuiceFilter.localContext.get(); + if (context == null) { + throw new OutOfScopeException("Not in a request scope"); + } + return context; + } + + private static RequestScoper transferNonHttpRequest() { + final Context context = requestScopeContext.get(); + if (context == null) { + throw new OutOfScopeException("Not in a request scope"); + } + return context; + } + + /** + * Returns true if {@code binding} is request-scoped. If the binding is a {@link + * com.google.inject.spi.LinkedKeyBinding linked key binding} and belongs to an injector (i. e. it + * was retrieved via {@link Injector#getBinding Injector.getBinding()}), then this method will + * also return true if the target binding is request-scoped. + * + * @since 4.0 + */ + public static boolean isRequestScoped(Binding binding) { + return Scopes.isScoped(binding, ServletScopes.REQUEST, RequestScoped.class); + } + + /** + * Scopes the given callable inside a request scope. This is not the same as the HTTP request + * scope, but is used if no HTTP request scope is in progress. In this way, keys can be scoped + * as @RequestScoped and exist in non-HTTP requests (for example: RPC requests) as well as in HTTP + * request threads. + * + *

The returned callable will throw a {@link ScopingException} when called if there is a + * request scope already active on the current thread. + * + * @param callable code to be executed which depends on the request scope. Typically in another + * thread, but not necessarily so. + * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To + * seed a key with null, use {@code null} as the value. + * @return a callable that when called will run inside the a request scope that exposes the + * instances in the {@code seedMap} as scoped keys. + * @since 3.0 + */ + public static Callable scopeRequest(Callable callable, Map, Object> seedMap) { + return wrap(callable, scopeRequest(seedMap)); + } + + /** + * Returns an object that will apply request scope to a block of code. This is not the same as the + * HTTP request scope, but is used if no HTTP request scope is in progress. In this way, keys can + * be scoped as @RequestScoped and exist in non-HTTP requests (for example: RPC requests) as well + * as in HTTP request threads. + * + *

The returned object will throw a {@link ScopingException} when opened if there is a request + * scope already active on the current thread. + * + * @param seedMap the initial set of scoped instances for Guice to seed the request scope with. To + * seed a key with null, use {@code null} as the value. + * @return an object that when opened will initiate the request scope + * @since 4.1 + */ + public static RequestScoper scopeRequest(Map, Object> seedMap) { + Preconditions.checkArgument( + null != seedMap, "Seed map cannot be null, try passing in Collections.emptyMap() instead."); + + // Copy the seed values into our local scope map. + final Context context = new Context(); + Map, Object> validatedAndCanonicalizedMap = + Maps.transformEntries(seedMap, ServletScopes::validateAndCanonicalizeValue); + context.map.putAll(validatedAndCanonicalizedMap); + return new RequestScoper() { + @Override + public CloseableScope open() { + checkScopingState( + null == GuiceFilter.localContext.get(), + "An HTTP request is already in progress, cannot scope a new request in this thread."); + checkScopingState( + null == requestScopeContext.get(), + "A request scope is already in progress, cannot scope a new request in this thread."); + return context.open(); + } + }; + } + + /** + * Validates the key and object, ensuring the value matches the key type, and canonicalizing null + * objects to the null sentinel. + */ + private static Object validateAndCanonicalizeValue(Key key, Object object) { + if (object == null || object == NullObject.INSTANCE) { + return NullObject.INSTANCE; + } + + Preconditions.checkArgument( + key.getTypeLiteral().getRawType().isInstance(object), + "Value[%s] of type[%s] is not compatible with key[%s]", + object, + object.getClass().getName(), + key); + + return object; + } + + private static class Context implements RequestScoper { + final Map, Object> map = Maps.newHashMap(); + + // Synchronized to prevent two threads from using the same request + // scope concurrently. + final Lock lock = new ReentrantLock(); + + @Override + public CloseableScope open() { + lock.lock(); + final Context previous = requestScopeContext.get(); + requestScopeContext.set(this); + return new CloseableScope() { + @Override + public void close() { + requestScopeContext.set(previous); + lock.unlock(); + } + }; + } + } + + private static void checkScopingState(boolean condition, String msg) { + if (!condition) { + throw new ScopingException(msg); + } + } + + private static Callable wrap(Callable delegate, RequestScoper requestScoper) { + return () -> { + try (RequestScoper.CloseableScope scope = requestScoper.open()) { + return delegate.call(); + } + }; + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletUtils.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletUtils.java new file mode 100644 index 0000000000..8691256b67 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletUtils.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.net.UrlEscapers; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Some servlet utility methods. + * + * @author ntang@google.com (Michael Tang) + */ +final class ServletUtils { + private static final Splitter SLASH_SPLITTER = Splitter.on('/'); + private static final Joiner SLASH_JOINER = Joiner.on('/'); + + private ServletUtils() { + // private to prevent instantiation. + } + + /** + * Gets the context path relative path of the URI. Returns the path of the resource relative to + * the context path for a request's URI, or null if no path can be extracted. + * + *

Also performs url decoding and normalization of the path. + */ + // @Nullable + static String getContextRelativePath( + // @Nullable + final HttpServletRequest request) { + if (request != null) { + String contextPath = request.getContextPath(); + String requestURI = request.getRequestURI(); + if (contextPath.length() < requestURI.length()) { + String suffix = requestURI.substring(contextPath.length()); + return normalizePath(suffix); + } else if (requestURI.trim().length() > 0 && contextPath.length() == requestURI.length()) { + return "/"; + } + } + return null; + } + + /** Normalizes a path by unescaping all safe, percent encoded characters. */ + static String normalizePath(String path) { + StringBuilder sb = new StringBuilder(path.length()); + int queryStart = path.indexOf('?'); + String query = null; + if (queryStart != -1) { + query = path.substring(queryStart); + path = path.substring(0, queryStart); + } + // Normalize the path. we need to decode path segments, normalize and rejoin in order to + // 1. decode and normalize safe percent escaped characters. e.g. %70 -> 'p' + // 2. decode and interpret dangerous character sequences. e.g. /%2E/ -> '/./' -> '/' + // 3. preserve dangerous encoded characters. e.g. '/%2F/' -> '///' -> '/%2F' + List segments = new ArrayList<>(); + for (String segment : SLASH_SPLITTER.split(path)) { + // This decodes all non-special characters from the path segment. so if someone passes + // /%2E/foo we will normalize it to /./foo and then /foo + String normalized = + UrlEscapers.urlPathSegmentEscaper().escape(lenientDecode(segment, UTF_8, false)); + if (".".equals(normalized)) { + // skip + } else if ("..".equals(normalized)) { + if (segments.size() > 1) { + segments.remove(segments.size() - 1); + } + } else { + segments.add(normalized); + } + } + SLASH_JOINER.appendTo(sb, segments); + if (query != null) { + sb.append(query); + } + return sb.toString(); + } + + + /** + * Percent-decodes a US-ASCII string into a Unicode string. The specified encoding is used to + * determine what characters are represented by any consecutive sequences of the form + * "%XX". This is the lenient kind of decoding that will simply ignore and copy as-is any + * "%XX" sequence that is invalid (for example, "%HH"). + * + * @param string a percent-encoded US-ASCII string + * @param encoding a character encoding + * @param decodePlus boolean to indicate whether to decode '+' as ' ' + * @return a Unicode string + */ + private static String lenientDecode(String string, Charset encoding, boolean decodePlus) { + + checkNotNull(string); + checkNotNull(encoding); + + if (decodePlus) { + string = string.replace('+', ' '); + } + + int firstPercentPos = string.indexOf('%'); + + if (firstPercentPos < 0) { + return string; + } + + ByteAccumulator accumulator = new ByteAccumulator(string.length(), encoding); + StringBuilder builder = new StringBuilder(string.length()); + + if (firstPercentPos > 0) { + builder.append(string, 0, firstPercentPos); + } + + for (int srcPos = firstPercentPos; srcPos < string.length(); srcPos++) { + + char c = string.charAt(srcPos); + + if (c < 0x80) { // ASCII + boolean processed = false; + + if (c == '%' + && string.length() >= srcPos + 3 + && string.charAt(srcPos + 1) != '+' + && string.charAt(srcPos + 1) != '-') { + String hex = string.substring(srcPos + 1, srcPos + 3); + + try { + int encoded = Integer.parseInt(hex, 16); + + if (encoded >= 0) { + accumulator.append((byte) encoded); + srcPos += 2; + processed = true; + } + } catch (NumberFormatException ignore) { + // Expected case (badly formatted % group) + } + } + + if (!processed) { + if (accumulator.isEmpty()) { + // We're not accumulating elements of a multibyte encoded + // char, so just toss it right into the result string. + + builder.append(c); + } else { + accumulator.append((byte) c); + } + } + } else { // Non-ASCII + // A non-ASCII char marks the end of a multi-char encoding sequence, + // if one is in progress. + + accumulator.dumpTo(builder); + builder.append(c); + } + } + + accumulator.dumpTo(builder); + + return builder.toString(); + } + + /** Accumulates byte sequences while decoding strings, and encodes them into a StringBuilder. */ + private static class ByteAccumulator { + private byte[] bytes; + private int length; + private final Charset encoding; + + ByteAccumulator(int capacity, Charset encoding) { + this.bytes = new byte[Math.min(16, capacity)]; + this.encoding = encoding; + } + + void append(byte b) { + ensureCapacity(length + 1); + bytes[length++] = b; + } + + void dumpTo(StringBuilder dest) { + if (length != 0) { + dest.append(new String(bytes, 0, length, encoding)); + length = 0; + } + } + + boolean isEmpty() { + return length == 0; + } + + private void ensureCapacity(int minCapacity) { + int cap = bytes.length; + if (cap >= minCapacity) { + return; + } + int newCapacity = cap + (cap >> 1); // *1.5 + if (newCapacity < minCapacity) { + // we are close to overflowing, grow by smaller steps + newCapacity = minCapacity; + } + // in other cases, we will naturally throw an OOM from here + bytes = Arrays.copyOf(bytes, newCapacity); + } + } + +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletsModuleBuilder.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletsModuleBuilder.java new file mode 100644 index 0000000000..a11b7c5e56 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/ServletsModuleBuilder.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.internal.UniqueAnnotations; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import jakarta.servlet.http.HttpServlet; + +/** + * Builds the guice module that binds configured servlets, with their wrapper ServletDefinitions. Is + * part of the binding EDSL. Very similar to {@link FiltersModuleBuilder}. + * + * @author Dhanji R. Prasanna (dhanji@gmail.com) + */ +class ServletsModuleBuilder { + + private final Set servletUris = Sets.newHashSet(); + private final Binder binder; + + public ServletsModuleBuilder(Binder binder) { + this.binder = binder; + } + + //the first level of the EDSL-- + public ServletModule.ServletKeyBindingBuilder serve(List urlPatterns) { + return new ServletKeyBindingBuilderImpl(parsePatterns(UriPatternType.SERVLET, urlPatterns)); + } + + public ServletModule.ServletKeyBindingBuilder serveRegex(List regexes) { + return new ServletKeyBindingBuilderImpl(parsePatterns(UriPatternType.REGEX, regexes)); + } + + private List parsePatterns(UriPatternType type, List patterns) { + List patternMatchers = new ArrayList<>(); + for (String pattern : patterns) { + if (!servletUris.add(pattern)) { + binder + .skipSources(ServletModule.class, ServletsModuleBuilder.class) + .addError("More than one servlet was mapped to the same URI pattern: " + pattern); + } else { + UriPatternMatcher matcher = null; + try { + matcher = UriPatternType.get(type, pattern); + } catch (IllegalArgumentException iae) { + binder + .skipSources(ServletModule.class, ServletsModuleBuilder.class) + .addError("%s", iae.getMessage()); + } + if (matcher != null) { + patternMatchers.add(matcher); + } + } + } + return patternMatchers; + } + + //non-static inner class so it can access state of enclosing module class + class ServletKeyBindingBuilderImpl implements ServletModule.ServletKeyBindingBuilder { + private final List uriPatterns; + + private ServletKeyBindingBuilderImpl(List uriPatterns) { + this.uriPatterns = uriPatterns; + } + + @Override + public void with(Class servletKey) { + with(Key.get(servletKey)); + } + + @Override + public void with(Key servletKey) { + with(servletKey, new HashMap()); + } + + @Override + public void with(HttpServlet servlet) { + with(servlet, new HashMap()); + } + + @Override + public void with(Class servletKey, Map initParams) { + with(Key.get(servletKey), initParams); + } + + @Override + public void with(Key servletKey, Map initParams) { + with(servletKey, initParams, null); + } + + private void with( + Key servletKey, + Map initParams, + HttpServlet servletInstance) { + for (UriPatternMatcher pattern : uriPatterns) { + binder + .bind(Key.get(ServletDefinition.class, UniqueAnnotations.create())) + .toProvider(new ServletDefinition(servletKey, pattern, initParams, servletInstance)); + } + } + + @Override + public void with(HttpServlet servlet, Map initParams) { + Key servletKey = Key.get(HttpServlet.class, UniqueAnnotations.create()); + binder.bind(servletKey).toInstance(servlet); + with(servletKey, initParams, servlet); + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/SessionScoped.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/SessionScoped.java new file mode 100644 index 0000000000..edec259ed5 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/SessionScoped.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.ScopeAnnotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Apply this to implementation classes when you want one instance per session. + * + * @see com.google.inject.Scopes#SINGLETON + * @author crazybob@google.com (Bob Lee) + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ScopeAnnotation +public @interface SessionScoped {} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternMatcher.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternMatcher.java new file mode 100644 index 0000000000..55ee946da9 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternMatcher.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.jee; + +/** + * A general interface for matching a URI against a URI pattern. Guice-servlet provides regex and + * servlet-style pattern matching out of the box. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +interface UriPatternMatcher { + /** + * @param uri A "contextual" (i.e. relative) and "normalized" Request URI, *not* a complete one. + * @return Returns true if the uri matches the pattern. + */ + boolean matches(String uri); + + /** + * @param pattern The Path that this service pattern can match against. + * @return Returns a canonical servlet path from this pattern. For instance, if the pattern is + * {@code /home/*} then the path extracted will be {@code /home}. Each pattern matcher + * implementation must decide and publish what a canonical path represents. + *

NOTE(user): This method returns null for the regex pattern matcher. + */ + String extractPath(String pattern); + + /** Returns the type of pattern this is. */ + UriPatternType getPatternType(); + + /** Returns the original pattern that was registered. */ + String getOriginalPattern(); +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternType.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternType.java new file mode 100644 index 0000000000..2f169acc74 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/UriPatternType.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * An enumeration of the available URI-pattern matching styles + * + * @since 3.0 + */ +public enum UriPatternType { + SERVLET, + REGEX; + + static UriPatternMatcher get(UriPatternType type, String pattern) { + switch (type) { + case SERVLET: + return new ServletStyleUriPatternMatcher(pattern); + case REGEX: + return new RegexUriPatternMatcher(pattern); + default: + return null; + } + } + + private static String getUri(String uri) { + // Strip out the query, if it existed in the URI. See issue 379. + int queryIdx = uri.indexOf('?'); + if (queryIdx != -1) { + uri = uri.substring(0, queryIdx); + } + return uri; + } + + /** + * Matches URIs using the pattern grammar of the Servlet API and web.xml. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ + private static class ServletStyleUriPatternMatcher implements UriPatternMatcher { + private final String literal; + private final String originalPattern; + private final Kind patternKind; + + private static enum Kind { + PREFIX, + SUFFIX, + LITERAL, + } + + public ServletStyleUriPatternMatcher(String pattern) { + this.originalPattern = pattern; + if (pattern.startsWith("*")) { + this.literal = pattern.substring(1); + this.patternKind = Kind.PREFIX; + } else if (pattern.endsWith("*")) { + this.literal = pattern.substring(0, pattern.length() - 1); + this.patternKind = Kind.SUFFIX; + } else { + this.literal = pattern; + this.patternKind = Kind.LITERAL; + } + String normalized = ServletUtils.normalizePath(literal); + if (patternKind == Kind.PREFIX) { + normalized = "*" + normalized; + } else if (patternKind == Kind.SUFFIX) { + normalized = normalized + "*"; + } + if (!pattern.equals(normalized)) { + throw new IllegalArgumentException( + "Servlet patterns cannot contain escape patterns. Registered pattern: '" + + pattern + + "' normalizes to: '" + + normalized + + "'"); + } + } + + @Override + public boolean matches(String uri) { + if (null == uri) { + return false; + } + + uri = getUri(uri); + if (patternKind == Kind.PREFIX) { + return uri.endsWith(literal); + } else if (patternKind == Kind.SUFFIX) { + return uri.startsWith(literal); + } + + //else we need a complete match + return literal.equals(uri); + } + + @Override + public String extractPath(String path) { + if (patternKind == Kind.PREFIX) { + return null; + } else if (patternKind == Kind.SUFFIX) { + String extract = literal; + + //trim the trailing '/' + if (extract.endsWith("/")) { + extract = extract.substring(0, extract.length() - 1); + } + + return extract; + } + + //else treat as literal + return path; + } + + @Override + public UriPatternType getPatternType() { + return UriPatternType.SERVLET; + } + + @Override + public String getOriginalPattern() { + return originalPattern; + } + } + + /** + * Matches URIs using a regular expression. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ + private static class RegexUriPatternMatcher implements UriPatternMatcher { + private final Pattern pattern; + private final String originalPattern; + + public RegexUriPatternMatcher(String pattern) { + this.originalPattern = pattern; + try { + this.pattern = Pattern.compile(pattern); + } catch (PatternSyntaxException pse) { + throw new IllegalArgumentException("Invalid regex pattern: " + pse.getMessage()); + } + } + + @Override + public boolean matches(String uri) { + return null != uri && this.pattern.matcher(getUri(uri)).matches(); + } + + @Override + public String extractPath(String path) { + Matcher matcher = pattern.matcher(path); + if (matcher.matches() && matcher.groupCount() >= 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; + } + + @Override + public UriPatternType getPatternType() { + return UriPatternType.REGEX; + } + + @Override + public String getOriginalPattern() { + return originalPattern; + } + } +} diff --git a/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/package-info.java b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/package-info.java new file mode 100644 index 0000000000..ddfd0d62b1 --- /dev/null +++ b/extensions/jakarta-servlet/src/com/google/inject/servlet/jee/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 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. + */ + +/** + * Servlet API scopes, bindings and registration; this extension requires {@code guice-servlet.jar}. + * + *

Apply {@link com.google.inject.servlet.jee.GuiceFilter} to any servlets which will use the servlet + * scopes. Install {@link com.google.inject.servlet.jee.ServletModule} into your {@link + * com.google.inject.Injector} to install everything at once. + */ +package com.google.inject.servlet.jee; diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/AllTests.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/AllTests.java new file mode 100644 index 0000000000..35363f55eb --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/AllTests.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.jee; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** @author dhanji@gmail.com (Dhanji R. Prasanna) */ +public class AllTests { + + public static Test suite() { + TestSuite suite = new TestSuite(); + + // Filter tests. + suite.addTestSuite(EdslTest.class); + suite.addTestSuite(FilterDefinitionTest.class); + suite.addTestSuite(FilterDispatchIntegrationTest.class); + suite.addTestSuite(FilterPipelineTest.class); + + // Servlet + integration tests. + suite.addTestSuite(ServletModuleTest.class); + suite.addTestSuite(ServletTest.class); + suite.addTestSuite(ServletDefinitionTest.class); + suite.addTestSuite(ServletDefinitionPathsTest.class); + suite.addTestSuite(ServletPipelineRequestDispatcherTest.class); + suite.addTestSuite(ServletDispatchIntegrationTest.class); + suite.addTestSuite(InvalidScopeBindingTest.class); + suite.addTestSuite(ContinuingHttpServletRequestTest.class); + + // Varargs URL mapping tests. + suite.addTestSuite(VarargsFilterDispatchIntegrationTest.class); + suite.addTestSuite(VarargsServletDispatchIntegrationTest.class); + + // Multiple modules tests. + suite.addTestSuite(MultiModuleDispatchIntegrationTest.class); + + // Extension SPI tests. + suite.addTestSuite(ExtensionSpiTest.class); + + suite.addTestSuite(UriPatternTypeTest.class); + + return suite; + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/BUILD b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/BUILD new file mode 100644 index 0000000000..014227851d --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/BUILD @@ -0,0 +1,48 @@ +# Copyright 2022 Google Inc. All rights reserved. +# Author: sameb@google.com (Sam Berlin) +load("@rules_java//java:defs.bzl", "java_library") +load("//:test_defs.bzl", "guice_test_suites") + +package(default_testonly = 1) + +java_library( + name = "tests", + srcs = glob(["**/*.java"]), + javacopts = ["-Xep:FutureReturnValueIgnored:OFF"], + plugins = [ + ], + deps = [ + "//core/src/com/google/inject", + "//core/test/com/google/inject:testsupport", + "//extensions/servlet/src/com/google/inject/servlet", + "//third_party/java/easymock", + "//third_party/java/guava/base", + "//third_party/java/guava/collect", + "//third_party/java/junit", + "//third_party/java/servlet/servlet_api", + ], +) + +guice_test_suites( + name = "gen_tests", + sizes = [ + "small", + "medium", + ], + deps = [":tests"], +) + +[guice_test_suites( + name = "gen_tests_%s" % include_stack_trace_option, + args = [ + "--guice_include_stack_traces=%s" % include_stack_trace_option, + ], + sizes = [ + "small", + "medium", + ], + suffix = "_stack_trace_%s" % include_stack_trace_option, + deps = [":tests"], +) for include_stack_trace_option in [ + "OFF", +]] diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContextPathTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContextPathTest.java new file mode 100644 index 0000000000..c445b6a604 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContextPathTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.createControl; +import static org.easymock.EasyMock.expect; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Scopes; +import com.google.inject.name.Names; +import java.io.IOException; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; +import org.easymock.IMocksControl; + +/** Tests to make sure that servlets with a context path are handled right. */ +public class ContextPathTest extends TestCase { + + @Inject + @Named("foo") + private TestServlet fooServlet; + + @Inject + @Named("bar") + private TestServlet barServlet; + + private IMocksControl globalControl; + private Injector injector; + private ServletContext servletContext; + private FilterConfig filterConfig; + private GuiceFilter guiceFilter; + + @Override + public final void setUp() throws Exception { + injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + bind(TestServlet.class) + .annotatedWith(Names.named("foo")) + .to(TestServlet.class) + .in(Scopes.SINGLETON); + bind(TestServlet.class) + .annotatedWith(Names.named("bar")) + .to(TestServlet.class) + .in(Scopes.SINGLETON); + serve("/foo/*").with(Key.get(TestServlet.class, Names.named("foo"))); + serve("/bar/*").with(Key.get(TestServlet.class, Names.named("bar"))); + // TODO: add a filter(..) call and validate it is correct + } + }); + injector.injectMembers(this); + + assertNotNull(fooServlet); + assertNotNull(barServlet); + assertNotSame(fooServlet, barServlet); + + globalControl = createControl(); + servletContext = globalControl.createMock(ServletContext.class); + filterConfig = globalControl.createMock(FilterConfig.class); + + expect(servletContext.getAttribute(GuiceServletContextListener.INJECTOR_NAME)) + .andReturn(injector) + .anyTimes(); + expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes(); + + globalControl.replay(); + + guiceFilter = new GuiceFilter(); + guiceFilter.init(filterConfig); + } + + @Override + public final void tearDown() { + assertNotNull(fooServlet); + assertNotNull(barServlet); + + fooServlet = null; + barServlet = null; + + guiceFilter.destroy(); + guiceFilter = null; + + injector = null; + + filterConfig = null; + servletContext = null; + + globalControl.verify(); + } + + public void testSimple() throws Exception { + IMocksControl testControl = createControl(); + TestFilterChain testFilterChain = new TestFilterChain(); + HttpServletRequest req = testControl.createMock(HttpServletRequest.class); + HttpServletResponse res = testControl.createMock(HttpServletResponse.class); + + expect(req.getMethod()).andReturn("GET").anyTimes(); + expect(req.getRequestURI()).andReturn("/bar/foo").anyTimes(); + expect(req.getServletPath()).andReturn("/bar/foo").anyTimes(); + expect(req.getContextPath()).andReturn("").anyTimes(); + + testControl.replay(); + + guiceFilter.doFilter(req, res, testFilterChain); + + assertFalse(testFilterChain.isTriggered()); + assertFalse(fooServlet.isTriggered()); + assertTrue(barServlet.isTriggered()); + + testControl.verify(); + } + + // + // each of the following "runTest" calls takes three path parameters: + // + // The value of "getRequestURI()" + // The value of "getServletPath()" + // The value of "getContextPath()" + // + // these values have been captured using a filter in Apache Tomcat 6.0.32 + // and are used for real-world values that a servlet container would send into + // the GuiceFilter. + // + // the remaining three booleans are: + // + // True if the request gets passed down the filter chain + // True if the request hits the "foo" servlet + // True if the request hits the "bar" sevlet + // + // After adjusting the request URI for the web app deployment location, all + // calls + // should always produce the same result. + // + + // ROOT Web app, using Tomcat default servlet + public void testRootDefault() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and /bar/*). + runTest("/", "/", "", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/bar/", "/bar/", "", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/foo/xxx", "/foo/xxx", "", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/xxx", "/xxx", "", true, false, false); + } + + // ROOT Web app, using explicit backing servlet mounted at /* + public void testRootExplicit() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and /bar/*). + runTest("/", "", "", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/bar/", "", "", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/foo/xxx", "", "", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/xxx", "", "", true, false, false); + } + + // ROOT Web app, using two backing servlets, mounted at /bar/* and /foo/* + public void testRootSpecific() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and /bar/*). + runTest("/", "/", "", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/bar/", "/bar", "", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/foo/xxx", "/foo", "", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/xxx", "/xxx", "", true, false, false); + } + + // Web app located at /webtest, using Tomcat default servlet + public void testWebtestDefault() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and /bar/*). + runTest("/webtest/", "/", "/webtest", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/webtest/bar/", "/bar/", "/webtest", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/webtest/foo/xxx", "/foo/xxx", "/webtest", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/webtest/xxx", "/xxx", "/webtest", true, false, false); + } + + // Web app located at /webtest, using explicit backing servlet mounted at /* + public void testWebtestExplicit() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and /bar/*). + runTest("/webtest/", "", "/webtest", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/webtest/bar/", "", "/webtest", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/webtest/foo/xxx", "", "/webtest", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/webtest/xxx", "", "/webtest", true, false, false); + } + + // Web app located at /webtest, using two backing servlets, mounted at /bar/* + // and /foo/* + public void testWebtestSpecific() throws Exception { + // fetching /. Should go up the filter chain (only mappings on /foo/* and + // /bar/*). + runTest("/webtest/", "/", "/webtest", true, false, false); + // fetching /bar/. Should hit the bar servlet + runTest("/webtest/bar/", "/bar", "/webtest", false, false, true); + // fetching /foo/xxx. Should hit the foo servlet + runTest("/webtest/foo/xxx", "/foo", "/webtest", false, true, false); + // fetching /xxx. Should go up the chain + runTest("/webtest/xxx", "/xxx", "/webtest", true, false, false); + } + + private void runTest( + final String requestURI, + final String servletPath, + final String contextPath, + final boolean filterResult, + final boolean fooResult, + final boolean barResult) + throws Exception { + IMocksControl testControl = createControl(); + + barServlet.clear(); + fooServlet.clear(); + + TestFilterChain testFilterChain = new TestFilterChain(); + HttpServletRequest req = testControl.createMock(HttpServletRequest.class); + HttpServletResponse res = testControl.createMock(HttpServletResponse.class); + + expect(req.getMethod()).andReturn("GET").anyTimes(); + expect(req.getRequestURI()).andReturn(requestURI).anyTimes(); + expect(req.getServletPath()).andReturn(servletPath).anyTimes(); + expect(req.getContextPath()).andReturn(contextPath).anyTimes(); + + testControl.replay(); + + guiceFilter.doFilter(req, res, testFilterChain); + + assertEquals(filterResult, testFilterChain.isTriggered()); + assertEquals(fooResult, fooServlet.isTriggered()); + assertEquals(barResult, barServlet.isTriggered()); + + testControl.verify(); + } + + public static class TestServlet extends HttpServlet { + private boolean triggered = false; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) { + triggered = true; + } + + public boolean isTriggered() { + return triggered; + } + + public void clear() { + triggered = false; + } + } + + public static class TestFilterChain implements FilterChain { + private boolean triggered = false; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + triggered = true; + } + + public boolean isTriggered() { + return triggered; + } + + public void clear() { + triggered = false; + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingHttpServletRequestTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingHttpServletRequestTest.java new file mode 100644 index 0000000000..98e0869fb0 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingHttpServletRequestTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +public class ContinuingHttpServletRequestTest extends TestCase { + + private static final String TEST_VALUE_1 = "testValue1"; + private static final String TEST_VALUE_2 = "testValue2"; + private static final int DEFAULT_MAX_AGE = new Cookie("dummy", "").getMaxAge(); + + public void testReturnNullCookiesIfDelegateHasNoNull() { + HttpServletRequest delegate = createMock(HttpServletRequest.class); + expect(delegate.getCookies()).andStubReturn(null); + + replay(delegate); + + assertNull(new ContinuingHttpServletRequest(delegate).getCookies()); + + verify(delegate); + } + + public void testReturnDelegateCookies() { + Cookie[] cookies = + new Cookie[] {new Cookie("testName1", TEST_VALUE_1), new Cookie("testName2", "testValue2")}; + HttpServletRequest delegate = createMock(HttpServletRequest.class); + expect(delegate.getCookies()).andStubReturn(cookies); + + replay(delegate); + + ContinuingHttpServletRequest continuingRequest = new ContinuingHttpServletRequest(delegate); + + assertCookieArraysEqual(cookies, continuingRequest.getCookies()); + + // Now mutate the original cookies, this shouldnt be reflected in the continued request. + cookies[0].setValue("INVALID"); + cookies[1].setValue("INVALID"); + cookies[1].setMaxAge(123); + + try { + assertCookieArraysEqual(cookies, continuingRequest.getCookies()); + throw new Error(); + } catch (AssertionFailedError e) { + // Expected. + } + + // Verify that they remain equal to the original values. + assertEquals(TEST_VALUE_1, continuingRequest.getCookies()[0].getValue()); + assertEquals(TEST_VALUE_2, continuingRequest.getCookies()[1].getValue()); + assertEquals(DEFAULT_MAX_AGE, continuingRequest.getCookies()[1].getMaxAge()); + + // Perform a snapshot of the snapshot. + ContinuingHttpServletRequest furtherContinuingRequest = + new ContinuingHttpServletRequest(continuingRequest); + + // The cookies should be fixed. + assertCookieArraysEqual(continuingRequest.getCookies(), furtherContinuingRequest.getCookies()); + + verify(delegate); + } + + private static void assertCookieArraysEqual(Cookie[] one, Cookie[] two) { + assertEquals(one.length, two.length); + for (int i = 0; i < one.length; i++) { + Cookie cookie = one[i]; + assertCookieEquality(cookie, two[i]); + } + } + + private static void assertCookieEquality(Cookie one, Cookie two) { + assertEquals(one.getName(), two.getName()); + assertEquals(one.getComment(), two.getComment()); + assertEquals(one.getDomain(), two.getDomain()); + assertEquals(one.getPath(), two.getPath()); + assertEquals(one.getValue(), two.getValue()); + assertEquals(one.getMaxAge(), two.getMaxAge()); + assertEquals(one.getSecure(), two.getSecure()); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingRequestIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingRequestIntegrationTest.java new file mode 100644 index 0000000000..b9dc2ec970 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ContinuingRequestIntegrationTest.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; + +/** Tests continuation of requests */ + +public class ContinuingRequestIntegrationTest extends TestCase { + private static final String PARAM_VALUE = "there"; + private static final String PARAM_NAME = "hi"; + + private final AtomicBoolean failed = new AtomicBoolean(false); + private final AbstractExecutorService sameThreadExecutor = + new AbstractExecutorService() { + @Override + public void shutdown() {} + + @Override + public List shutdownNow() { + return ImmutableList.of(); + } + + @Override + public boolean isShutdown() { + return true; + } + + @Override + public boolean isTerminated() { + return true; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void execute(Runnable command) { + command.run(); + } + + @Override + public Future submit(Callable task) { + try { + task.call(); + fail(); + } catch (Exception e) { + // Expected. + assertTrue(e instanceof IllegalStateException); + failed.set(true); + } + + return null; + } + }; + + private ExecutorService executor; + private Injector injector; + + @Override + protected void tearDown() throws Exception { + injector.getInstance(GuiceFilter.class).destroy(); + } + + public final void testRequestContinuesInOtherThread() + throws ServletException, IOException, InterruptedException { + executor = Executors.newSingleThreadExecutor(); + + injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/*").with(ContinuingServlet.class); + + bind(ExecutorService.class).toInstance(executor); + } + }); + + FilterConfig filterConfig = createMock(FilterConfig.class); + expect(filterConfig.getServletContext()).andReturn(createMock(ServletContext.class)); + + GuiceFilter guiceFilter = injector.getInstance(GuiceFilter.class); + + HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getRequestURI()).andReturn("/"); + expect(request.getContextPath()).andReturn("").anyTimes(); + expect(request.getMethod()).andReturn("GET"); + expect(request.getCookies()).andReturn(new Cookie[0]); + + FilterChain filterChain = createMock(FilterChain.class); + expect(request.getParameter(PARAM_NAME)).andReturn(PARAM_VALUE); + + replay(request, filterConfig, filterChain); + + guiceFilter.init(filterConfig); + guiceFilter.doFilter(request, createMock(HttpServletResponse.class), filterChain); + + // join. + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + assertEquals(PARAM_VALUE, injector.getInstance(OffRequestCallable.class).value); + verify(request, filterConfig, filterChain); + } + + public final void testRequestContinuationDiesInHttpRequestThread() + throws ServletException, IOException, InterruptedException { + executor = sameThreadExecutor; + injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/*").with(ContinuingServlet.class); + + bind(ExecutorService.class).toInstance(executor); + + bind(SomeObject.class); + } + }); + + FilterConfig filterConfig = createMock(FilterConfig.class); + expect(filterConfig.getServletContext()).andReturn(createMock(ServletContext.class)); + + GuiceFilter guiceFilter = injector.getInstance(GuiceFilter.class); + + HttpServletRequest request = createMock(HttpServletRequest.class); + + expect(request.getRequestURI()).andReturn("/"); + expect(request.getContextPath()).andReturn("").anyTimes(); + + expect(request.getMethod()).andReturn("GET"); + expect(request.getCookies()).andReturn(new Cookie[0]); + FilterChain filterChain = createMock(FilterChain.class); + + replay(request, filterConfig, filterChain); + + guiceFilter.init(filterConfig); + guiceFilter.doFilter(request, createMock(HttpServletResponse.class), filterChain); + + // join. + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + assertTrue(failed.get()); + assertFalse(PARAM_VALUE.equals(injector.getInstance(OffRequestCallable.class).value)); + + verify(request, filterConfig, filterChain); + } + + @RequestScoped + public static class SomeObject {} + + @Singleton + public static class ContinuingServlet extends HttpServlet { + @Inject + OffRequestCallable callable; + @Inject ExecutorService executorService; + + private SomeObject someObject; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + assertNull(someObject); + + // Seed with someobject. + someObject = new SomeObject(); + Callable task = + ServletScopes.continueRequest( + callable, ImmutableMap., Object>of(Key.get(SomeObject.class), someObject)); + + executorService.submit(task); + } + } + + @Singleton + public static class OffRequestCallable implements Callable { + @Inject Provider request; + @Inject Provider response; + @Inject Provider someObject; + + public String value; + + @Override + public String call() throws Exception { + assertNull(response.get()); + + // Inside this request, we should always get the same instance. + assertSame(someObject.get(), someObject.get()); + + return value = request.get().getParameter(PARAM_NAME); + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyFilterImpl.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyFilterImpl.java new file mode 100644 index 0000000000..a850259850 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyFilterImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 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.jee; + +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * Used in unit tests to verify the EDSL. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class DummyFilterImpl implements Filter { + int num; + + public DummyFilterImpl() {} + + public DummyFilterImpl(int num) { + this.num = num; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException {} + + @Override + public void destroy() {} +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyServlet.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyServlet.java new file mode 100644 index 0000000000..aaf8c27d6d --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/DummyServlet.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.Singleton; +import jakarta.servlet.http.HttpServlet; + +/** + * Used in unit tests to verify the EDSL. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@Singleton +public class DummyServlet extends HttpServlet {} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/EdslTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/EdslTest.java new file mode 100644 index 0000000000..698df24bb1 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/EdslTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Singleton; +import com.google.inject.Stage; +import java.util.HashMap; +import junit.framework.TestCase; + +/** + * Sanity checks the EDSL and resultant bound module(s). + * + * @author Dhanji R. Prasanna (dhanji gmail com) + */ +public class EdslTest extends TestCase { + + public final void testExplicitBindingsWorksWithGuiceServlet() { + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + binder().requireExplicitBindings(); + } + }, + new ServletModule() { + @Override + protected void configureServlets() { + bind(DummyServlet.class).in(Singleton.class); + serve("/*").with(DummyServlet.class); + } + }); + + assertNotNull(injector.getInstance(DummyServlet.class)); + } + + public final void testConfigureServlets() { + + //the various possible config calls-- + Module webModule = + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(DummyFilterImpl.class); + filter("*.html").through(DummyFilterImpl.class); + filter("/*").through(Key.get(DummyFilterImpl.class)); + filter("/*").through(new DummyFilterImpl()); + + filter("*.html").through(DummyFilterImpl.class, new HashMap()); + + filterRegex("/person/[0-9]*").through(DummyFilterImpl.class); + filterRegex("/person/[0-9]*") + .through(DummyFilterImpl.class, new HashMap()); + + filterRegex("/person/[0-9]*").through(Key.get(DummyFilterImpl.class)); + filterRegex("/person/[0-9]*") + .through(Key.get(DummyFilterImpl.class), new HashMap()); + + filterRegex("/person/[0-9]*").through(new DummyFilterImpl()); + filterRegex("/person/[0-9]*") + .through(new DummyFilterImpl(), new HashMap()); + + serve("/1/*").with(DummyServlet.class); + serve("/2/*").with(Key.get(DummyServlet.class)); + serve("/3/*").with(new DummyServlet()); + serve("/4/*").with(DummyServlet.class, new HashMap()); + + serve("*.htm").with(Key.get(DummyServlet.class)); + serve("*.html").with(Key.get(DummyServlet.class), new HashMap()); + + serveRegex("/person/[0-8]*").with(DummyServlet.class); + serveRegex("/person/[0-9]*").with(DummyServlet.class, new HashMap()); + + serveRegex("/person/[0-6]*").with(Key.get(DummyServlet.class)); + serveRegex("/person/[0-9]/2/*") + .with(Key.get(DummyServlet.class), new HashMap()); + + serveRegex("/person/[0-5]*").with(new DummyServlet()); + serveRegex("/person/[0-9]/3/*").with(new DummyServlet(), new HashMap()); + } + }; + + //verify that it doesn't blow up! + Guice.createInjector(Stage.TOOL, webModule); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ExtensionSpiTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ExtensionSpiTest.java new file mode 100644 index 0000000000..0e40534319 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ExtensionSpiTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.UriPatternType.REGEX; +import static com.google.inject.servlet.jee.UriPatternType.SERVLET; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.servlet.jee.ServletSpiVisitor.Params; +import com.google.inject.name.Names; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import java.util.Iterator; +import java.util.List; +import junit.framework.TestCase; + +/** + * A very basic test that servletmodule works with bindings. + * + * @author sameb@google.com (Sam Berlin) + */ +public class ExtensionSpiTest extends TestCase { + + private DummyFilterImpl dummyFilter1 = new DummyFilterImpl(); + private DummyFilterImpl dummyFilter2 = new DummyFilterImpl(); + private DummyFilterImpl dummyFilter3 = new DummyFilterImpl(); + private DummyFilterImpl dummyFilter4 = new DummyFilterImpl(); + + private DummyServlet dummyServlet1 = new DummyServlet(); + private DummyServlet dummyServlet2 = new DummyServlet(); + private DummyServlet dummyServlet3 = new DummyServlet(); + private DummyServlet dummyServlet4 = new DummyServlet(); + + public final void testSpiOnElements() { + ServletSpiVisitor visitor = new ServletSpiVisitor(false); + int count = 0; + for (Element element : Elements.getElements(new Module())) { + if (element instanceof Binding) { + int actual = ((Binding) element).acceptTargetVisitor(visitor); + assertEquals(count++, actual); + } + } + validateVisitor(visitor); + } + + public final void testSpiOnInjector() { + ServletSpiVisitor visitor = new ServletSpiVisitor(true); + int count = 0; + Injector injector = Guice.createInjector(new Module()); + for (Binding binding : injector.getBindings().values()) { + int actual = binding.acceptTargetVisitor(visitor); + assertEquals(count++, actual); + } + validateVisitor(visitor); + } + + private void validateVisitor(ServletSpiVisitor visitor) { + assertEquals(48, visitor.currentCount - visitor.otherCount); + + // This is the expected param list, in order.. + List expected = + ImmutableList.of( + new Params("/class", Key.get(DummyFilterImpl.class), ImmutableMap.of(), SERVLET), + new Params("/class/2", Key.get(DummyFilterImpl.class), ImmutableMap.of(), SERVLET), + new Params( + "/key", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of(), + SERVLET), + new Params( + "/key/2", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of(), + SERVLET), + new Params("/instance", dummyFilter1, ImmutableMap.of(), SERVLET), + new Params("/instance/2", dummyFilter1, ImmutableMap.of(), SERVLET), + new Params( + "/class/keyvalues", + Key.get(DummyFilterImpl.class), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/class/keyvalues/2", + Key.get(DummyFilterImpl.class), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/key/keyvalues", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/key/keyvalues/2", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/instance/keyvalues", dummyFilter2, ImmutableMap.of("key", "value"), SERVLET), + new Params( + "/instance/keyvalues/2", dummyFilter2, ImmutableMap.of("key", "value"), SERVLET), + new Params("/class[0-9]", Key.get(DummyFilterImpl.class), ImmutableMap.of(), REGEX), + new Params("/class[0-9]/2", Key.get(DummyFilterImpl.class), ImmutableMap.of(), REGEX), + new Params( + "/key[0-9]", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of(), + REGEX), + new Params( + "/key[0-9]/2", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of(), + REGEX), + new Params("/instance[0-9]", dummyFilter3, ImmutableMap.of(), REGEX), + new Params("/instance[0-9]/2", dummyFilter3, ImmutableMap.of(), REGEX), + new Params( + "/class[0-9]/keyvalues", + Key.get(DummyFilterImpl.class), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/class[0-9]/keyvalues/2", + Key.get(DummyFilterImpl.class), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/key[0-9]/keyvalues", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/key[0-9]/keyvalues/2", + Key.get(DummyFilterImpl.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/instance[0-9]/keyvalues", dummyFilter4, ImmutableMap.of("key", "value"), REGEX), + new Params( + "/instance[0-9]/keyvalues/2", dummyFilter4, ImmutableMap.of("key", "value"), REGEX), + new Params("/class", Key.get(DummyServlet.class), ImmutableMap.of(), SERVLET), + new Params("/class/2", Key.get(DummyServlet.class), ImmutableMap.of(), SERVLET), + new Params( + "/key", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of(), + SERVLET), + new Params( + "/key/2", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of(), + SERVLET), + new Params("/instance", dummyServlet1, ImmutableMap.of(), SERVLET), + new Params("/instance/2", dummyServlet1, ImmutableMap.of(), SERVLET), + new Params( + "/class/keyvalues", + Key.get(DummyServlet.class), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/class/keyvalues/2", + Key.get(DummyServlet.class), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/key/keyvalues", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/key/keyvalues/2", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + SERVLET), + new Params( + "/instance/keyvalues", dummyServlet2, ImmutableMap.of("key", "value"), SERVLET), + new Params( + "/instance/keyvalues/2", dummyServlet2, ImmutableMap.of("key", "value"), SERVLET), + new Params("/class[0-9]", Key.get(DummyServlet.class), ImmutableMap.of(), REGEX), + new Params("/class[0-9]/2", Key.get(DummyServlet.class), ImmutableMap.of(), REGEX), + new Params( + "/key[0-9]", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of(), + REGEX), + new Params( + "/key[0-9]/2", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of(), + REGEX), + new Params("/instance[0-9]", dummyServlet3, ImmutableMap.of(), REGEX), + new Params("/instance[0-9]/2", dummyServlet3, ImmutableMap.of(), REGEX), + new Params( + "/class[0-9]/keyvalues", + Key.get(DummyServlet.class), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/class[0-9]/keyvalues/2", + Key.get(DummyServlet.class), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/key[0-9]/keyvalues", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/key[0-9]/keyvalues/2", + Key.get(DummyServlet.class, Names.named("foo")), + ImmutableMap.of("key", "value"), + REGEX), + new Params( + "/instance[0-9]/keyvalues", dummyServlet4, ImmutableMap.of("key", "value"), REGEX), + new Params( + "/instance[0-9]/keyvalues/2", + dummyServlet4, + ImmutableMap.of("key", "value"), + REGEX)); + + assertEquals(expected.size(), visitor.actual.size()); + Iterator actualIterator = visitor.actual.iterator(); + int i = 0; + for (Params param : expected) { + assertEquals("wrong " + i++ + "th param", param, actualIterator.next()); + } + } + + private class Module extends ServletModule { + @Override + protected void configureServlets() { + binder().requireExplicitBindings(); + + filter("/class", "/class/2").through(DummyFilterImpl.class); + filter("/key", "/key/2").through(Key.get(DummyFilterImpl.class, Names.named("foo"))); + filter("/instance", "/instance/2").through(dummyFilter1); + filter("/class/keyvalues", "/class/keyvalues/2") + .through(DummyFilterImpl.class, ImmutableMap.of("key", "value")); + filter("/key/keyvalues", "/key/keyvalues/2") + .through( + Key.get(DummyFilterImpl.class, Names.named("foo")), ImmutableMap.of("key", "value")); + filter("/instance/keyvalues", "/instance/keyvalues/2") + .through(dummyFilter2, ImmutableMap.of("key", "value")); + + filterRegex("/class[0-9]", "/class[0-9]/2").through(DummyFilterImpl.class); + filterRegex("/key[0-9]", "/key[0-9]/2") + .through(Key.get(DummyFilterImpl.class, Names.named("foo"))); + filterRegex("/instance[0-9]", "/instance[0-9]/2").through(dummyFilter3); + filterRegex("/class[0-9]/keyvalues", "/class[0-9]/keyvalues/2") + .through(DummyFilterImpl.class, ImmutableMap.of("key", "value")); + filterRegex("/key[0-9]/keyvalues", "/key[0-9]/keyvalues/2") + .through( + Key.get(DummyFilterImpl.class, Names.named("foo")), ImmutableMap.of("key", "value")); + filterRegex("/instance[0-9]/keyvalues", "/instance[0-9]/keyvalues/2") + .through(dummyFilter4, ImmutableMap.of("key", "value")); + + serve("/class", "/class/2").with(DummyServlet.class); + serve("/key", "/key/2").with(Key.get(DummyServlet.class, Names.named("foo"))); + serve("/instance", "/instance/2").with(dummyServlet1); + serve("/class/keyvalues", "/class/keyvalues/2") + .with(DummyServlet.class, ImmutableMap.of("key", "value")); + serve("/key/keyvalues", "/key/keyvalues/2") + .with(Key.get(DummyServlet.class, Names.named("foo")), ImmutableMap.of("key", "value")); + serve("/instance/keyvalues", "/instance/keyvalues/2") + .with(dummyServlet2, ImmutableMap.of("key", "value")); + + serveRegex("/class[0-9]", "/class[0-9]/2").with(DummyServlet.class); + serveRegex("/key[0-9]", "/key[0-9]/2").with(Key.get(DummyServlet.class, Names.named("foo"))); + serveRegex("/instance[0-9]", "/instance[0-9]/2").with(dummyServlet3); + serveRegex("/class[0-9]/keyvalues", "/class[0-9]/keyvalues/2") + .with(DummyServlet.class, ImmutableMap.of("key", "value")); + serveRegex("/key[0-9]/keyvalues", "/key[0-9]/keyvalues/2") + .with(Key.get(DummyServlet.class, Names.named("foo")), ImmutableMap.of("key", "value")); + serveRegex("/instance[0-9]/keyvalues", "/instance[0-9]/keyvalues/2") + .with(dummyServlet4, ImmutableMap.of("key", "value")); + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDefinitionTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDefinitionTest.java new file mode 100644 index 0000000000..6b5c061969 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDefinitionTest.java @@ -0,0 +1,321 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.spi.BindingScopingVisitor; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * Tests the lifecycle of the encapsulated {@link FilterDefinition} class. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@SuppressWarnings("unchecked") // Safe because mocks can only return the required types. +public class FilterDefinitionTest extends TestCase { + public final void testFilterInitAndConfig() throws ServletException { + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + + final MockFilter mockFilter = new MockFilter(); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(Filter.class))).andReturn(binding); + + expect(injector.getInstance(Key.get(Filter.class))).andReturn(mockFilter).anyTimes(); + + replay(binding, injector); + + // some init params + //noinspection SSBasedInspection + final Map initParams = + new ImmutableMap.Builder() + .put("ahsd", "asdas24dok") + .put("ahssd", "asdasd124ok") + .buildOrThrow(); + + ServletContext servletContext = createMock(ServletContext.class); + final String contextName = "thing__!@@44"; + expect(servletContext.getServletContextName()).andReturn(contextName); + + replay(servletContext); + + String pattern = "/*"; + final FilterDefinition filterDef = + new FilterDefinition( + Key.get(Filter.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + initParams, + null); + filterDef.init(servletContext, injector, Sets.newIdentityHashSet()); + + assertTrue(filterDef.getFilter() instanceof MockFilter); + final FilterConfig filterConfig = mockFilter.getConfig(); + assertTrue(null != filterConfig); + assertEquals(contextName, filterConfig.getServletContext().getServletContextName()); + assertEquals(filterConfig.getFilterName(), Key.get(Filter.class).toString()); + + final Enumeration names = filterConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + + assertTrue(initParams.containsKey(name)); + assertEquals(filterConfig.getInitParameter(name), initParams.get(name)); + } + + verify(binding, injector, servletContext); + } + + public final void testFilterCreateDispatchDestroy() throws ServletException, IOException { + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + + final MockFilter mockFilter = new MockFilter(); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(Filter.class))).andReturn(binding); + + expect(injector.getInstance(Key.get(Filter.class))).andReturn(mockFilter).anyTimes(); + + expect(request.getRequestURI()).andReturn("/index.html"); + expect(request.getContextPath()).andReturn("").anyTimes(); + + replay(injector, binding, request); + + String pattern = "/*"; + final FilterDefinition filterDef = + new FilterDefinition( + Key.get(Filter.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + //should fire on mockfilter now + filterDef.init(createMock(ServletContext.class), injector, Sets.newIdentityHashSet()); + assertTrue(filterDef.getFilter() instanceof MockFilter); + + assertTrue("Init did not fire", mockFilter.isInit()); + + Filter matchingFilter = filterDef.getFilterIfMatching(request); + assertSame(mockFilter, matchingFilter); + + final boolean proceed[] = new boolean[1]; + matchingFilter.doFilter( + request, + null, + new FilterChainInvocation(null, null, null) { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + proceed[0] = true; + } + }); + + assertTrue("Filter did not proceed down chain", proceed[0]); + + filterDef.destroy(Sets.newIdentityHashSet()); + assertTrue("Destroy did not fire", mockFilter.isDestroy()); + + verify(injector, request); + } + + public final void testFilterCreateDispatchDestroySupressChain() + throws ServletException, IOException { + + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + + final MockFilter mockFilter = + new MockFilter() { + @Override + public void doFilter( + ServletRequest servletRequest, + ServletResponse servletResponse, + FilterChain filterChain) { + //suppress rest of chain... + } + }; + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(Filter.class))).andReturn(binding); + + expect(injector.getInstance(Key.get(Filter.class))).andReturn(mockFilter).anyTimes(); + + expect(request.getRequestURI()).andReturn("/index.html"); + expect(request.getContextPath()).andReturn("").anyTimes(); + + replay(injector, binding, request); + + String pattern = "/*"; + final FilterDefinition filterDef = + new FilterDefinition( + Key.get(Filter.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + //should fire on mockfilter now + filterDef.init(createMock(ServletContext.class), injector, Sets.newIdentityHashSet()); + assertTrue(filterDef.getFilter() instanceof MockFilter); + + assertTrue("init did not fire", mockFilter.isInit()); + + Filter matchingFilter = filterDef.getFilterIfMatching(request); + assertSame(mockFilter, matchingFilter); + + final boolean proceed[] = new boolean[1]; + matchingFilter.doFilter( + request, + null, + new FilterChainInvocation(null, null, null) { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + proceed[0] = true; + } + }); + + assertFalse("filter did not suppress chain", proceed[0]); + + filterDef.destroy(Sets.newIdentityHashSet()); + assertTrue("destroy did not fire", mockFilter.isDestroy()); + + verify(injector, request); + } + + public void testGetFilterIfMatching() throws ServletException { + String pattern = "/*"; + final FilterDefinition filterDef = + new FilterDefinition( + Key.get(Filter.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + HttpServletRequest servletRequest = createMock(HttpServletRequest.class); + ServletContext servletContext = createMock(ServletContext.class); + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + + final MockFilter mockFilter = + new MockFilter() { + @Override + public void doFilter( + ServletRequest servletRequest, + ServletResponse servletResponse, + FilterChain filterChain) { + //suppress rest of chain... + } + }; + expect(injector.getBinding(Key.get(Filter.class))).andReturn(binding); + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getInstance(Key.get(Filter.class))).andReturn(mockFilter).anyTimes(); + + expect(servletRequest.getContextPath()).andReturn("/a_context_path"); + expect(servletRequest.getRequestURI()).andReturn("/a_context_path/test.html"); + + replay(servletRequest, binding, injector); + filterDef.init(servletContext, injector, Sets.newIdentityHashSet()); + Filter filter = filterDef.getFilterIfMatching(servletRequest); + assertSame(filter, mockFilter); + verify(servletRequest, binding, injector); + } + + public void testGetFilterIfMatchingNotMatching() throws ServletException { + String pattern = "/*"; + final FilterDefinition filterDef = + new FilterDefinition( + Key.get(Filter.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + HttpServletRequest servletRequest = createMock(HttpServletRequest.class); + ServletContext servletContext = createMock(ServletContext.class); + Injector injector = createMock(Injector.class); + @SuppressWarnings("unchecked") // Safe because mock will only ever return Filter + Binding binding = createMock(Binding.class); + + final MockFilter mockFilter = + new MockFilter() { + @Override + public void doFilter( + ServletRequest servletRequest, + ServletResponse servletResponse, + FilterChain filterChain) { + //suppress rest of chain... + } + }; + expect(injector.getBinding(Key.get(Filter.class))).andReturn(binding); + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getInstance(Key.get(Filter.class))).andReturn(mockFilter).anyTimes(); + + expect(servletRequest.getContextPath()).andReturn("/a_context_path"); + expect(servletRequest.getRequestURI()).andReturn("/test.html"); + + replay(servletRequest, binding, injector); + filterDef.init(servletContext, injector, Sets.newIdentityHashSet()); + Filter filter = filterDef.getFilterIfMatching(servletRequest); + assertNull(filter); + verify(servletRequest, binding, injector); + } + + private static class MockFilter implements Filter { + private boolean init; + private boolean destroy; + private FilterConfig config; + + @Override + public void init(FilterConfig filterConfig) { + init = true; + + this.config = filterConfig; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + //proceed + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroy = true; + } + + public boolean isInit() { + return init; + } + + public boolean isDestroy() { + return destroy; + } + + public FilterConfig getConfig() { + return config; + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDispatchIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDispatchIntegrationTest.java new file mode 100644 index 0000000000..28bec5c52f --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterDispatchIntegrationTest.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST; +import static com.google.inject.servlet.jee.ServletTestUtils.newFakeHttpServletRequest; +import static com.google.inject.servlet.jee.ServletTestUtils.newNoOpFilterChain; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; + +/** + * This tests that filter stage of the pipeline dispatches correctly to guice-managed filters. + * + *

WARNING(dhanji): Non-parallelizable test =( + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class FilterDispatchIntegrationTest extends TestCase { + private static int inits, doFilters, destroys; + + private IMocksControl control; + + @Override + public final void setUp() { + inits = 0; + doFilters = 0; + destroys = 0; + control = EasyMock.createControl(); + GuiceFilter.reset(); + } + + public final void testDispatchRequestToManagedPipeline() throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + filter("*.html").through(TestFilter.class); + filter("/*").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + + // Bind a servlet + serve("*.html").with(TestServlet.class); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + // create ourselves a mock request with test URI + HttpServletRequest requestMock = control.createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + requestMock.setAttribute(REQUEST_DISPATCHER_REQUEST, true); + requestMock.removeAttribute(REQUEST_DISPATCHER_REQUEST); + + HttpServletResponse responseMock = control.createMock(HttpServletResponse.class); + expect(responseMock.isCommitted()).andReturn(false).anyTimes(); + responseMock.resetBuffer(); + expectLastCall().anyTimes(); + + FilterChain filterChain = control.createMock(FilterChain.class); + + //dispatch request + control.replay(); + pipeline.dispatch(requestMock, responseMock, filterChain); + pipeline.destroyPipeline(); + control.verify(); + + TestServlet servlet = injector.getInstance(TestServlet.class); + assertEquals(2, servlet.processedUris.size()); + assertTrue(servlet.processedUris.contains("/index.html")); + assertTrue(servlet.processedUris.contains(TestServlet.FORWARD_TO)); + + assertTrue( + "lifecycle states did not" + + " fire correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 3 && destroys == 1); + } + + public final void testDispatchThatNoFiltersFire() throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/public/*").through(TestFilter.class); + filter("*.html").through(TestFilter.class); + filter("*.xml").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = control.createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.xhtml").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + FilterChain filterChain = control.createMock(FilterChain.class); + filterChain.doFilter(requestMock, null); + control.replay(); + pipeline.dispatch(requestMock, null, filterChain); + pipeline.destroyPipeline(); + control.verify(); + + assertTrue( + "lifecycle states did not " + + "fire correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 0 && destroys == 1); + } + + public final void testDispatchFilterPipelineWithRegexMatching() + throws ServletException, IOException { + + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filterRegex("/[A-Za-z]*").through(TestFilter.class); + filterRegex("/index").through(TestFilter.class); + //these filters should never fire + filterRegex("\\w").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = control.createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + // dispatch request + FilterChain filterChain = control.createMock(FilterChain.class); + filterChain.doFilter(requestMock, null); + control.replay(); + pipeline.dispatch(requestMock, null, filterChain); + pipeline.destroyPipeline(); + control.verify(); + + assertTrue( + "lifecycle states did not fire " + + "correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 2 && destroys == 1); + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) { + inits++; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + doFilters++; + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroys++; + } + } + + public final void testFilterBypass() throws ServletException, IOException { + + final Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/protected/*").through(TestFilter.class); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + assertEquals(1, inits); + + runRequestForPath(pipeline, "/./protected/resource", true); + runRequestForPath(pipeline, "/protected/../resource", false); + runRequestForPath(pipeline, "/protected/../protected/resource", true); + + assertEquals(0, destroys); + pipeline.destroyPipeline(); + assertEquals(1, destroys); + } + + private void runRequestForPath(FilterPipeline pipeline, String value, boolean matches) + throws IOException, ServletException { + assertEquals(0, doFilters); + //create ourselves a mock request with test URI + HttpServletRequest requestMock = control.createMock(HttpServletRequest.class); + expect(requestMock.getRequestURI()).andReturn(value).anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + // dispatch request + FilterChain filterChain = control.createMock(FilterChain.class); + filterChain.doFilter(requestMock, null); + control.replay(); + pipeline.dispatch(requestMock, null, filterChain); + control.verify(); + control.reset(); + if (matches) { + assertEquals("filter was not run", 1, doFilters); + doFilters = 0; + } else { + assertEquals("filter was run", 0, doFilters); + } + } + + @Singleton + public static class TestServlet extends HttpServlet { + public static final String FORWARD_FROM = "/index.html"; + public static final String FORWARD_TO = "/forwarded.html"; + public List processedUris = new ArrayList<>(); + + @Override + protected void service( + HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + String requestUri = httpServletRequest.getRequestURI(); + processedUris.add(requestUri); + + // If the client is requesting /index.html then we forward to /forwarded.html + if (FORWARD_FROM.equals(requestUri)) { + httpServletRequest + .getRequestDispatcher(FORWARD_TO) + .forward(httpServletRequest, httpServletResponse); + } + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); + } + } + + public void testFilterOrder() throws Exception { + AtomicInteger counter = new AtomicInteger(); + final CountFilter f1 = new CountFilter(counter); + final CountFilter f2 = new CountFilter(counter); + + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/").through(f1); + install( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/").through(f2); + } + }); + } + }); + + HttpServletRequest request = newFakeHttpServletRequest(); + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + pipeline.dispatch(request, null, newNoOpFilterChain()); + assertEquals(0, f1.calledAt); + assertEquals(1, f2.calledAt); + } + + /** A filter that keeps count of when it was called by increment a counter. */ + private static class CountFilter implements Filter { + private final AtomicInteger counter; + private int calledAt = -1; + + public CountFilter(AtomicInteger counter) { + this.counter = counter; + } + + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws ServletException, IOException { + if (calledAt != -1) { + fail("not expecting to be called twice"); + } + calledAt = counter.getAndIncrement(); + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) {} + } + + public final void testFilterExceptionPrunesStack() throws Exception { + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/").through(TestFilter.class); + filter("/nothing").through(TestFilter.class); + filter("/").through(ThrowingFilter.class); + } + }); + + HttpServletRequest request = newFakeHttpServletRequest(); + FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + try { + pipeline.dispatch(request, null, null); + fail("expected exception"); + } catch (ServletException ex) { + for (StackTraceElement element : ex.getStackTrace()) { + String className = element.getClassName(); + assertTrue( + "was: " + element, + !className.equals(FilterChainInvocation.class.getName()) + && !className.equals(FilterDefinition.class.getName())); + } + } + } + + public final void testServletExceptionPrunesStack() throws Exception { + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/").through(TestFilter.class); + filter("/nothing").through(TestFilter.class); + serve("/").with(ThrowingServlet.class); + } + }); + + HttpServletRequest request = newFakeHttpServletRequest(); + FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + try { + pipeline.dispatch(request, null, null); + fail("expected exception"); + } catch (ServletException ex) { + for (StackTraceElement element : ex.getStackTrace()) { + String className = element.getClassName(); + assertTrue( + "was: " + element, + !className.equals(FilterChainInvocation.class.getName()) + && !className.equals(FilterDefinition.class.getName())); + } + } + } + + @Singleton + private static class ThrowingServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException { + throw new ServletException("failure!"); + } + } + + @Singleton + private static class ThrowingFilter implements Filter { + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws ServletException { + throw new ServletException("we failed!"); + } + + @Override + public void init(FilterConfig filterConfig) {} + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterPipelineTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterPipelineTest.java new file mode 100644 index 0000000000..ba030faabf --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/FilterPipelineTest.java @@ -0,0 +1,121 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.isNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * This is a basic whitebox test that verifies the glue between GuiceFilter and + * ManagedFilterPipeline is working. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class FilterPipelineTest extends TestCase { + + @Override + public final void setUp() { + GuiceFilter.reset(); + + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + filter("*.html").through(TestFilter.class); + filter("/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(NeverFilter.class)); + filter("/public/login/*").through(Key.get(NeverFilter.class)); + } + }); + } + + @Override + public final void tearDown() { + GuiceFilter.reset(); + } + + public final void testDispatchThruGuiceFilter() throws ServletException, IOException { + + //create mocks + FilterConfig filterConfig = createMock(FilterConfig.class); + ServletContext servletContext = createMock(ServletContext.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + FilterChain proceedingFilterChain = createMock(FilterChain.class); + + //begin mock script *** + + expect(filterConfig.getServletContext()).andReturn(servletContext).once(); + + expect(request.getRequestURI()).andReturn("/public/login.jsp").anyTimes(); + expect(request.getContextPath()).andReturn("").anyTimes(); + + //at the end, proceed down webapp's normal filter chain + proceedingFilterChain.doFilter(isA(HttpServletRequest.class), (ServletResponse) isNull()); + expectLastCall().once(); + + //run mock script *** + replay(filterConfig, servletContext, request, proceedingFilterChain); + + final GuiceFilter webFilter = new GuiceFilter(); + + webFilter.init(filterConfig); + webFilter.doFilter(request, null, proceedingFilterChain); + webFilter.destroy(); + + //assert expectations + verify(filterConfig, servletContext, request, proceedingFilterChain); + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() {} + } + + @Singleton + public static class NeverFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { + fail("This filter should never have fired"); + } + + @Override + public void destroy() {} + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InjectedFilterPipelineTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InjectedFilterPipelineTest.java new file mode 100644 index 0000000000..776f14995e --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InjectedFilterPipelineTest.java @@ -0,0 +1,172 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.isNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * Exactly the same as {@linkplain FilterPipelineTest} except that we test + * that the static pipeline is not used. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class InjectedFilterPipelineTest extends TestCase { + private Injector injector1; + private Injector injector2; + + @Override + public final void setUp() { + injector1 = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + filter("*.html").through(TestFilter.class); + filter("/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(NeverFilter.class)); + filter("/public/login/*").through(Key.get(NeverFilter.class)); + } + }); + + // Test second injector with exactly opposite pipeline config + injector2 = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + // These filters should never fire + filter("*.html").through(NeverFilter.class); + filter("/non-jsp/*").through(Key.get(NeverFilter.class)); + + // only these filters fire. + filter("/index/*").through(Key.get(TestFilter.class)); + filter("/public/login/*").through(Key.get(TestFilter.class)); + } + }); + } + + @Override + public final void tearDown() {} + + public final void testDispatchThruInjectedGuiceFilter() throws ServletException, IOException { + + //create mocks + FilterConfig filterConfig = createMock(FilterConfig.class); + ServletContext servletContext = createMock(ServletContext.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + FilterChain proceedingFilterChain = createMock(FilterChain.class); + + //begin mock script *** + + expect(filterConfig.getServletContext()).andReturn(servletContext).once(); + + expect(request.getRequestURI()) + .andReturn("/non-jsp/login.html") // use a path that will fail in injector2 + .anyTimes(); + expect(request.getContextPath()).andReturn("").anyTimes(); + + //at the end, proceed down webapp's normal filter chain + proceedingFilterChain.doFilter(isA(HttpServletRequest.class), (ServletResponse) isNull()); + expectLastCall().once(); + + //run mock script *** + replay(filterConfig, servletContext, request, proceedingFilterChain); + + GuiceFilter webFilter = injector1.getInstance(GuiceFilter.class); + + webFilter.init(filterConfig); + webFilter.doFilter(request, null, proceedingFilterChain); + webFilter.destroy(); + + //assert expectations + verify(filterConfig, servletContext, request, proceedingFilterChain); + + // reset mocks and run them against the other injector + reset(filterConfig, servletContext, request, proceedingFilterChain); + + // Create a second proceeding filter chain + FilterChain proceedingFilterChain2 = createMock(FilterChain.class); + + //begin mock script *** + + expect(filterConfig.getServletContext()).andReturn(servletContext).once(); + expect(request.getRequestURI()) + .andReturn("/public/login/login.jsp") // use a path that will fail in injector1 + .anyTimes(); + expect(request.getContextPath()).andReturn("").anyTimes(); + + //at the end, proceed down webapp's normal filter chain + proceedingFilterChain2.doFilter(isA(HttpServletRequest.class), (ServletResponse) isNull()); + expectLastCall().once(); + + // Never fire on this pipeline + replay(filterConfig, servletContext, request, proceedingFilterChain2, proceedingFilterChain); + + webFilter = injector2.getInstance(GuiceFilter.class); + + webFilter.init(filterConfig); + webFilter.doFilter(request, null, proceedingFilterChain2); + webFilter.destroy(); + + // Verify that we have not crossed the streams, Venkman! + verify(filterConfig, servletContext, request, proceedingFilterChain, proceedingFilterChain2); + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() {} + } + + @Singleton + public static class NeverFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + fail("This filter should never have fired"); + } + + @Override + public void destroy() {} + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InvalidScopeBindingTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InvalidScopeBindingTest.java new file mode 100644 index 0000000000..4f22677c71 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/InvalidScopeBindingTest.java @@ -0,0 +1,102 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; + +import com.google.inject.Guice; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import junit.framework.TestCase; + +/** + * Ensures that an error is thrown if a Servlet or Filter is bound under any scope other than + * singleton, explicitly. + * + * @author dhanji@gmail.com + */ +public class InvalidScopeBindingTest extends TestCase { + + @Override + protected void tearDown() throws Exception { + GuiceFilter.reset(); + } + + public final void testServletInNonSingletonScopeThrowsServletException() { + GuiceFilter guiceFilter = new GuiceFilter(); + + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/*").with(MyNonSingletonServlet.class); + } + }); + + ServletException se = null; + try { + guiceFilter.init(createMock(FilterConfig.class)); + } catch (ServletException e) { + se = e; + } finally { + assertNotNull("Servlet exception was not thrown with wrong scope binding", se); + } + } + + public final void testFilterInNonSingletonScopeThrowsServletException() { + GuiceFilter guiceFilter = new GuiceFilter(); + + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/*").through(MyNonSingletonFilter.class); + } + }); + + ServletException se = null; + try { + guiceFilter.init(createMock(FilterConfig.class)); + } catch (ServletException e) { + se = e; + } finally { + assertNotNull("Servlet exception was not thrown with wrong scope binding", se); + } + } + + public final void testHappyCaseFilter() { + GuiceFilter guiceFilter = new GuiceFilter(); + + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + // Annotated scoping variant. + filter("/*").through(MySingletonFilter.class); + + // Explicit scoping variant. + bind(DummyFilterImpl.class).in(Scopes.SINGLETON); + filter("/*").through(DummyFilterImpl.class); + } + }); + + ServletException se = null; + try { + guiceFilter.init(createMock(FilterConfig.class)); + } catch (ServletException e) { + se = e; + } finally { + assertNull("Servlet exception was thrown with correct scope binding", se); + } + } + + @RequestScoped + public static class MyNonSingletonServlet extends HttpServlet {} + + @SessionScoped + public static class MyNonSingletonFilter extends DummyFilterImpl {} + + @Singleton + public static class MySingletonFilter extends DummyFilterImpl {} +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultiModuleDispatchIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultiModuleDispatchIntegrationTest.java new file mode 100644 index 0000000000..68ce32c76e --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultiModuleDispatchIntegrationTest.java @@ -0,0 +1,114 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * This tests that filter stage of the pipeline dispatches correctly to guice-managed filters with + * multiple modules. + * + *

WARNING(dhanji): Non-parallelizable test =( + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class MultiModuleDispatchIntegrationTest extends TestCase { + private static int inits, doFilters, destroys; + + @Override + public final void setUp() { + inits = 0; + doFilters = 0; + destroys = 0; + + GuiceFilter.reset(); + } + + public final void testDispatchRequestToManagedPipeline() throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + + // These filters should never fire + filter("*.jsp").through(Key.get(TestFilter.class)); + } + }, + new ServletModule() { + + @Override + protected void configureServlets() { + filter("*.html").through(TestFilter.class); + filter("/*").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not" + + " fire correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 3 && destroys == 1); + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + doFilters++; + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroys++; + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultipleServletInjectorsTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultipleServletInjectorsTest.java new file mode 100644 index 0000000000..5b747ecbff --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/MultipleServletInjectorsTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.http.HttpServlet; +import junit.framework.TestCase; + +/** + * This gorgeous test asserts that multiple servlet pipelines can run in the SAME JVM. booya. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class MultipleServletInjectorsTest extends TestCase { + + private Injector injectorOne; + private Injector injectorTwo; + + public final void testTwoInjectors() { + ServletContext fakeContextOne = createMock(ServletContext.class); + ServletContext fakeContextTwo = createMock(ServletContext.class); + + fakeContextOne.setAttribute(eq(GuiceServletContextListener.INJECTOR_NAME), isA(Injector.class)); + expectLastCall().once(); + + fakeContextTwo.setAttribute(eq(GuiceServletContextListener.INJECTOR_NAME), isA(Injector.class)); + expectLastCall().once(); + + replay(fakeContextOne); + + // Simulate the start of a servlet container. + new GuiceServletContextListener() { + + @Override + protected Injector getInjector() { + // Cache this injector in the test for later testing... + return injectorOne = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + // This creates a ManagedFilterPipeline internally... + serve("/*").with(DummyServlet.class); + } + }); + } + }.contextInitialized(new ServletContextEvent(fakeContextOne)); + + ServletContext contextOne = injectorOne.getInstance(ServletContext.class); + assertNotNull(contextOne); + + // Now simulate a second injector with a slightly different config. + replay(fakeContextTwo); + new GuiceServletContextListener() { + + @Override + protected Injector getInjector() { + return injectorTwo = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + // This creates a ManagedFilterPipeline internally... + filter("/8").through(DummyFilterImpl.class); + + serve("/*").with(HttpServlet.class); + } + }); + } + }.contextInitialized(new ServletContextEvent(fakeContextTwo)); + + ServletContext contextTwo = injectorTwo.getInstance(ServletContext.class); + + // Make sure they are different. + assertNotNull(contextTwo); + assertNotSame(contextOne, contextTwo); + + // Make sure they are as expected + assertSame(fakeContextOne, contextOne); + assertSame(fakeContextTwo, contextTwo); + + // Make sure they are consistent. + assertSame(contextOne, injectorOne.getInstance(ServletContext.class)); + assertSame(contextTwo, injectorTwo.getInstance(ServletContext.class)); + + verify(fakeContextOne, fakeContextTwo); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ScopeRequestIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ScopeRequestIntegrationTest.java new file mode 100644 index 0000000000..f6e1f3b1fe --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ScopeRequestIntegrationTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import com.google.inject.name.Names; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.ServletException; +import junit.framework.TestCase; + +/** Tests continuation of requests */ + +public class ScopeRequestIntegrationTest extends TestCase { + private static final String A_VALUE = "thereaoskdao"; + private static final String A_DIFFERENT_VALUE = "hiaoskd"; + + private static final String SHOULDNEVERBESEEN = "Shouldneverbeseen!"; + + public final void testNonHttpRequestScopedCallable() + throws ServletException, IOException, InterruptedException, ExecutionException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + // We use servlet module here because we want to test that @RequestScoped + // behaves properly with the non-HTTP request scope logic. + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + bindConstant().annotatedWith(Names.named(SomeObject.INVALID)).to(SHOULDNEVERBESEEN); + bind(SomeObject.class).in(RequestScoped.class); + } + }); + + SomeObject someObject = new SomeObject(A_VALUE); + OffRequestCallable offRequestCallable = injector.getInstance(OffRequestCallable.class); + executor + .submit( + ServletScopes.scopeRequest( + offRequestCallable, + ImmutableMap., Object>of(Key.get(SomeObject.class), someObject))) + .get(); + + assertSame(injector.getInstance(OffRequestCallable.class), offRequestCallable); + + // Make sure the value was passed on. + assertEquals(someObject.value, offRequestCallable.value); + assertFalse(SHOULDNEVERBESEEN.equals(someObject.value)); + + // Now create a new request and assert that the scopes don't cross. + someObject = new SomeObject(A_DIFFERENT_VALUE); + executor + .submit( + ServletScopes.scopeRequest( + offRequestCallable, + ImmutableMap., Object>of(Key.get(SomeObject.class), someObject))) + .get(); + + assertSame(injector.getInstance(OffRequestCallable.class), offRequestCallable); + + // Make sure the value was passed on. + assertEquals(someObject.value, offRequestCallable.value); + assertFalse(SHOULDNEVERBESEEN.equals(someObject.value)); + executor.shutdown(); + executor.awaitTermination(2, TimeUnit.SECONDS); + } + + public final void testWrongValueClasses() throws Exception { + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + bindConstant().annotatedWith(Names.named(SomeObject.INVALID)).to(SHOULDNEVERBESEEN); + bind(SomeObject.class).in(RequestScoped.class); + } + }); + + OffRequestCallable offRequestCallable = injector.getInstance(OffRequestCallable.class); + try { + ServletScopes.scopeRequest( + offRequestCallable, ImmutableMap., Object>of(Key.get(SomeObject.class), "Boo!")); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals( + "Value[Boo!] of type[java.lang.String] is not compatible with key[" + + Key.get(SomeObject.class) + + "]", + iae.getMessage()); + } + } + + public final void testNullReplacement() throws Exception { + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + bindConstant().annotatedWith(Names.named(SomeObject.INVALID)).to(SHOULDNEVERBESEEN); + bind(SomeObject.class).in(RequestScoped.class); + } + }); + + Callable callable = injector.getInstance(Caller.class); + try { + assertNotNull(callable.call()); + fail(); + } catch (ProvisionException pe) { + assertTrue(pe.getCause() instanceof OutOfScopeException); + } + + // Validate that an actual null entry in the map results in a null injected object. + Map, Object> map = Maps.newHashMap(); + map.put(Key.get(SomeObject.class), null); + callable = ServletScopes.scopeRequest(injector.getInstance(Caller.class), map); + assertNull(callable.call()); + } + + @RequestScoped + public static class SomeObject { + private static final String INVALID = "invalid"; + + @Inject + public SomeObject(@Named(INVALID) String value) { + this.value = value; + } + + private final String value; + } + + @Singleton + public static class OffRequestCallable implements Callable { + @Inject Provider someObject; + + public String value; + + @Override + public String call() throws Exception { + // Inside this request, we should always get the same instance. + assertSame(someObject.get(), someObject.get()); + + value = someObject.get().value; + assertFalse(SHOULDNEVERBESEEN.equals(value)); + + return value; + } + } + + private static class Caller implements Callable { + @Inject Provider someObject; + + @Override + public SomeObject call() throws Exception { + return someObject.get(); + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionPathsTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionPathsTest.java new file mode 100644 index 0000000000..7399b64cad --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionPathsTest.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.spi.BindingScopingVisitor; +import java.io.IOException; +import java.util.HashMap; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; + +/** + * Ensures servlet spec compliance for CGI-style variables and general path/pattern matching. + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@SuppressWarnings("unchecked") // Safe because mock will only ever return HttpServlet +public class ServletDefinitionPathsTest extends TestCase { + + // Data-driven test. + public final void testServletPathMatching() throws IOException, ServletException { + servletPath("/index.html", "*.html", "/index.html"); + servletPath("/somewhere/index.html", "*.html", "/somewhere/index.html"); + servletPath("/somewhere/index.html", "/*", ""); + servletPath("/index.html", "/*", ""); + servletPath("/", "/*", ""); + servletPath("//", "/*", ""); + servletPath("/////", "/*", ""); + servletPath("", "/*", ""); + servletPath("/thing/index.html", "/thing/*", "/thing"); + servletPath("/thing/wing/index.html", "/thing/*", "/thing"); + } + + private void servletPath( + final String requestPath, String mapping, final String expectedServletPath) + throws IOException, ServletException { + + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + + final boolean[] run = new boolean[1]; + //get an instance of this servlet + expect(injector.getInstance(Key.get(HttpServlet.class))) + .andReturn( + new HttpServlet() { + + @Override + protected void service( + HttpServletRequest servletRequest, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + + final String path = servletRequest.getServletPath(); + assertEquals( + String.format("expected [%s] but was [%s]", expectedServletPath, path), + expectedServletPath, + path); + run[0] = true; + } + }); + + expect(request.getServletPath()).andReturn(requestPath); + + replay(injector, binding, request); + + ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, mapping), + new HashMap(), + null); + + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + servletDefinition.doService(request, response); + + assertTrue("Servlet did not run!", run[0]); + + verify(injector, binding, request); + } + + // Data-driven test. + public final void testPathInfoWithServletStyleMatching() throws IOException, ServletException { + pathInfoWithServletStyleMatching("/path/index.html", "/path", "/*", "/index.html", ""); + pathInfoWithServletStyleMatching( + "/path//hulaboo///index.html", "/path", "/*", "/hulaboo/index.html", ""); + pathInfoWithServletStyleMatching("/path/", "/path", "/*", "/", ""); + pathInfoWithServletStyleMatching("/path////////", "/path", "/*", "/", ""); + + // a servlet mapping of /thing/* + pathInfoWithServletStyleMatching("/path/thing////////", "/path", "/thing/*", "/", "/thing"); + pathInfoWithServletStyleMatching("/path/thing/stuff", "/path", "/thing/*", "/stuff", "/thing"); + pathInfoWithServletStyleMatching( + "/path/thing/stuff.html", "/path", "/thing/*", "/stuff.html", "/thing"); + pathInfoWithServletStyleMatching("/path/thing", "/path", "/thing/*", null, "/thing"); + + // see external issue 372 + pathInfoWithServletStyleMatching( + "/path/some/path/of.jsp", "/path", "/thing/*", null, "/some/path/of.jsp"); + + // *.xx style mapping + pathInfoWithServletStyleMatching("/path/thing.thing", "/path", "*.thing", null, "/thing.thing"); + pathInfoWithServletStyleMatching("/path///h.thing", "/path", "*.thing", null, "/h.thing"); + pathInfoWithServletStyleMatching( + "/path///...//h.thing", "/path", "*.thing", null, "/.../h.thing"); + pathInfoWithServletStyleMatching("/path/my/h.thing", "/path", "*.thing", null, "/my/h.thing"); + + // Encoded URLs + pathInfoWithServletStyleMatching("/path/index%2B.html", "/path", "/*", "/index+.html", ""); + pathInfoWithServletStyleMatching( + "/path/a%20file%20with%20spaces%20in%20name.html", + "/path", "/*", "/a file with spaces in name.html", ""); + pathInfoWithServletStyleMatching( + "/path/Tam%C3%A1s%20nem%20m%C3%A1s.html", "/path", "/*", "/Tamás nem más.html", ""); + } + + private void pathInfoWithServletStyleMatching( + final String requestUri, + final String contextPath, + String mapping, + final String expectedPathInfo, + final String servletPath) + throws IOException, ServletException { + + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + + final boolean[] run = new boolean[1]; + //get an instance of this servlet + expect(injector.getInstance(Key.get(HttpServlet.class))) + .andReturn( + new HttpServlet() { + + @Override + protected void service( + HttpServletRequest servletRequest, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + + final String path = servletRequest.getPathInfo(); + + if (null == expectedPathInfo) { + assertNull( + String.format("expected [%s] but was [%s]", expectedPathInfo, path), path); + } else { + assertEquals( + String.format("expected [%s] but was [%s]", expectedPathInfo, path), + expectedPathInfo, + path); + } + + //assert memoizer + //noinspection StringEquality + assertSame("memo field did not work", path, servletRequest.getPathInfo()); + + run[0] = true; + } + }); + + expect(request.getRequestURI()).andReturn(requestUri); + + expect(request.getServletPath()).andReturn(servletPath).anyTimes(); + + expect(request.getContextPath()).andReturn(contextPath); + + expect(request.getAttribute(REQUEST_DISPATCHER_REQUEST)).andReturn(null); + + replay(injector, binding, request); + + ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, mapping), + new HashMap(), + null); + + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + servletDefinition.doService(request, response); + + assertTrue("Servlet did not run!", run[0]); + + verify(injector, binding, request); + } + + // Data-driven test. + public final void testPathInfoWithRegexMatching() throws IOException, ServletException { + // first a mapping of /* + pathInfoWithRegexMatching("/path/index.html", "/path", "/(.)*", "/index.html", ""); + pathInfoWithRegexMatching( + "/path//hulaboo///index.html", "/path", "/(.)*", "/hulaboo/index.html", ""); + pathInfoWithRegexMatching("/path/", "/path", "/(.)*", "/", ""); + pathInfoWithRegexMatching("/path////////", "/path", "/(.)*", "/", ""); + + // a servlet mapping of /thing/* + pathInfoWithRegexMatching("/path/thing////////", "/path", "/thing/(.)*", "/", "/thing"); + pathInfoWithRegexMatching("/path/thing/stuff", "/path", "/thing/(.)*", "/stuff", "/thing"); + pathInfoWithRegexMatching( + "/path/thing/stuff.html", "/path", "/thing/(.)*", "/stuff.html", "/thing"); + pathInfoWithRegexMatching("/path/thing", "/path", "/thing/(.)*", null, "/thing"); + + // *.xx style mapping + pathInfoWithRegexMatching("/path/thing.thing", "/path", ".*\\.thing", null, "/thing.thing"); + pathInfoWithRegexMatching("/path///h.thing", "/path", ".*\\.thing", null, "/h.thing"); + pathInfoWithRegexMatching("/path///...//h.thing", "/path", ".*\\.thing", null, "/.../h.thing"); + pathInfoWithRegexMatching("/path/my/h.thing", "/path", ".*\\.thing", null, "/my/h.thing"); + + // path + pathInfoWithRegexMatching( + "/path/test.com/com.test.MyServletModule", + "", + "/path/[^/]+/(.*)", + "com.test.MyServletModule", + "/path/test.com/com.test.MyServletModule"); + + // Encoded URLs + pathInfoWithRegexMatching("/path/index%2B.html", "/path", "/(.)*", "/index+.html", ""); + pathInfoWithRegexMatching( + "/path/a%20file%20with%20spaces%20in%20name.html", + "/path", "/(.)*", "/a file with spaces in name.html", ""); + pathInfoWithRegexMatching( + "/path/Tam%C3%A1s%20nem%20m%C3%A1s.html", "/path", "/(.)*", "/Tamás nem más.html", ""); + } + + public final void pathInfoWithRegexMatching( + final String requestUri, + final String contextPath, + String mapping, + final String expectedPathInfo, + final String servletPath) + throws IOException, ServletException { + + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + HttpServletRequest request = createMock(HttpServletRequest.class); + HttpServletResponse response = createMock(HttpServletResponse.class); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + + final boolean[] run = new boolean[1]; + //get an instance of this servlet + expect(injector.getInstance(Key.get(HttpServlet.class))) + .andReturn( + new HttpServlet() { + + @Override + protected void service( + HttpServletRequest servletRequest, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + + final String path = servletRequest.getPathInfo(); + + if (null == expectedPathInfo) { + assertNull( + String.format("expected [%s] but was [%s]", expectedPathInfo, path), path); + } else { + assertEquals( + String.format("expected [%s] but was [%s]", expectedPathInfo, path), + expectedPathInfo, + path); + } + + //assert memoizer + //noinspection StringEquality + assertSame("memo field did not work", path, servletRequest.getPathInfo()); + + run[0] = true; + } + }); + + expect(request.getRequestURI()).andReturn(requestUri); + + expect(request.getServletPath()).andReturn(servletPath).anyTimes(); + + expect(request.getContextPath()).andReturn(contextPath); + + expect(request.getAttribute(REQUEST_DISPATCHER_REQUEST)).andReturn(null); + + replay(injector, binding, request); + + ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.REGEX, mapping), + new HashMap(), + null); + + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + servletDefinition.doService(request, response); + + assertTrue("Servlet did not run!", run[0]); + + verify(injector, binding, request); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionTest.java new file mode 100644 index 0000000000..0e88003499 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDefinitionTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.spi.BindingScopingVisitor; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Map; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; + +/** + * Basic unit test for lifecycle of a ServletDefinition (wrapper). + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +public class ServletDefinitionTest extends TestCase { + + @SuppressWarnings("unchecked") // Safe because mock will only ever return HttpServlet + public final void testServletInitAndConfig() throws ServletException { + Injector injector = createMock(Injector.class); + Binding binding = createMock(Binding.class); + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())) + .andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + final HttpServlet mockServlet = new HttpServlet() {}; + expect(injector.getInstance(Key.get(HttpServlet.class))).andReturn(mockServlet).anyTimes(); + + replay(injector, binding); + + // some init params + //noinspection SSBasedInspection + final Map initParams = + new ImmutableMap.Builder() + .put("ahsd", "asdas24dok") + .put("ahssd", "asdasd124ok") + .buildOrThrow(); + + String pattern = "/*"; + final ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + initParams, + null); + + ServletContext servletContext = createMock(ServletContext.class); + final String contextName = "thing__!@@44__SRV" + getClass(); + expect(servletContext.getServletContextName()).andReturn(contextName); + + replay(servletContext); + + servletDefinition.init(servletContext, injector, Sets.newIdentityHashSet()); + + assertNotNull(mockServlet.getServletContext()); + assertEquals(contextName, mockServlet.getServletContext().getServletContextName()); + assertEquals(Key.get(HttpServlet.class).toString(), mockServlet.getServletName()); + + final ServletConfig servletConfig = mockServlet.getServletConfig(); + final Enumeration names = servletConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + + assertTrue(initParams.containsKey(name)); + assertEquals(initParams.get(name), servletConfig.getInitParameter(name)); + } + + verify(injector, binding, servletContext); + } + + public void testServiceWithContextPath() throws IOException, ServletException { + String pattern = "/*"; + // some init params + Map initParams = + new ImmutableMap.Builder() + .put("ahsd", "asdas24dok") + .put("ahssd", "asdasd124ok") + .buildOrThrow(); + + final ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + initParams, + null); + HttpServletResponse servletResponse = createMock(HttpServletResponse.class); + HttpServletRequest servletRequest = createMock(HttpServletRequest.class); + + expect(servletRequest.getContextPath()).andReturn("/a_context_path"); + expect(servletRequest.getRequestURI()).andReturn("/test.html"); + replay(servletRequest, servletResponse); + servletDefinition.service(servletRequest, servletResponse); + verify(servletRequest, servletResponse); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDispatchIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDispatchIntegrationTest.java new file mode 100644 index 0000000000..10f0178ee3 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletDispatchIntegrationTest.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; + +/** + * Tests the FilterPipeline that dispatches to guice-managed servlets, is a full integration test, + * with a real injector. + * + * @author Dhanji R. Prasanna (dhanji gmail com) + */ +public class ServletDispatchIntegrationTest extends TestCase { + private static int inits, services, destroys, doFilters; + + @Override + public void setUp() { + inits = 0; + services = 0; + destroys = 0; + doFilters = 0; + + GuiceFilter.reset(); + } + + public final void testDispatchRequestToManagedPipelineServlets() + throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + serve("/*").with(TestServlet.class); + + // These servets should never fire... (ordering test) + serve("*.html").with(NeverServlet.class); + serve("/test/*").with(Key.get(NeverServlet.class)); + serve("/index/*").with(Key.get(NeverServlet.class)); + serve("*.jsp").with(Key.get(NeverServlet.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").times(1); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire correct number of times-- inits: " + + inits + + "; dos: " + + services + + "; destroys: " + + destroys, + inits == 2 && services == 1 && destroys == 2); + } + + public final void testDispatchRequestToManagedPipelineWithFilter() + throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + + serve("/*").with(TestServlet.class); + + // These servets should never fire... + serve("*.html").with(NeverServlet.class); + serve("/test/*").with(Key.get(NeverServlet.class)); + serve("/index/*").with(Key.get(NeverServlet.class)); + serve("*.jsp").with(Key.get(NeverServlet.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").times(2); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire correct number of times-- inits: " + + inits + + "; dos: " + + services + + "; destroys: " + + destroys + + "; doFilters: " + + doFilters, + inits == 3 && services == 1 && destroys == 3 && doFilters == 1); + } + + @Singleton + public static class TestServlet extends HttpServlet { + @Override + public void init(ServletConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + services++; + } + + @Override + public void destroy() { + destroys++; + } + } + + @Singleton + public static class NeverServlet extends HttpServlet { + @Override + public void init(ServletConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + fail("NeverServlet was fired, when it should not have been."); + } + + @Override + public void destroy() { + destroys++; + } + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + doFilters++; + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroys++; + } + } + + @Singleton + public static class ForwardingServlet extends HttpServlet { + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + + request.getRequestDispatcher("/blah.jsp").forward(servletRequest, servletResponse); + } + } + + @Singleton + public static class ForwardedServlet extends HttpServlet { + static int forwardedTo = 0; + + // Reset for test. + public ForwardedServlet() { + forwardedTo = 0; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + + assertTrue((Boolean) request.getAttribute(REQUEST_DISPATCHER_REQUEST)); + forwardedTo++; + } + } + + public void testForwardUsingRequestDispatcher() throws IOException, ServletException { + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/").with(ForwardingServlet.class); + serve("/blah.jsp").with(ForwardedServlet.class); + } + }); + + final HttpServletRequest requestMock = createMock(HttpServletRequest.class); + HttpServletResponse responseMock = createMock(HttpServletResponse.class); + expect(requestMock.getRequestURI()).andReturn("/").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + requestMock.setAttribute(REQUEST_DISPATCHER_REQUEST, true); + expect(requestMock.getAttribute(REQUEST_DISPATCHER_REQUEST)).andReturn(true); + requestMock.removeAttribute(REQUEST_DISPATCHER_REQUEST); + + expect(responseMock.isCommitted()).andReturn(false); + responseMock.resetBuffer(); + + replay(requestMock, responseMock); + + new GuiceFilter().doFilter(requestMock, responseMock, createMock(FilterChain.class)); + + assertEquals("Incorrect number of forwards", 1, ForwardedServlet.forwardedTo); + verify(requestMock, responseMock); + } + + public final void testQueryInRequestUri_regex() throws Exception { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filterRegex("(.)*\\.html").through(TestFilter.class); + + serveRegex("(.)*\\.html").with(TestServlet.class); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html?query=params").atLeastOnce(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + + pipeline.destroyPipeline(); + + verify(requestMock); + + assertEquals(1, doFilters); + assertEquals(1, services); + } + + public final void testQueryInRequestUri() throws Exception { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/index.html").through(TestFilter.class); + + serve("/index.html").with(TestServlet.class); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html?query=params").atLeastOnce(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + + pipeline.destroyPipeline(); + + verify(requestMock); + + assertEquals(1, doFilters); + assertEquals(1, services); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletModuleTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletModuleTest.java new file mode 100644 index 0000000000..6a78fb9705 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletModuleTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.Lists; +import com.google.inject.Binding; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.Elements; +import java.util.List; +import junit.framework.TestCase; + +/** + * Tests for ServletModule, to ensure it captures bindings correctly. + * + * @author sameb@google.com (Sam Berlin) + */ +public class ServletModuleTest extends TestCase { + + public void testServletModuleCallOutsideConfigure() { + try { + new ServletModule() { + { + serve("/*").with(DummyServlet.class); + } + }; + fail(); + } catch (IllegalStateException e) { + // Expected. + } + } + + public void testServletModuleReuse() { + Module module = new Module(); + Elements.getElements(module); // use the module once (to, say, introspect bindings) + Injector injector = Guice.createInjector(module); // use it again. + + Visitor visitor = new Visitor(); + // Validate only a single servlet binding & a single filter binding exist. + for (Binding binding : injector.getAllBindings().values()) { + binding.acceptTargetVisitor(visitor); + } + assertEquals( + "wrong linked servlets: " + visitor.linkedServlets, 0, visitor.linkedServlets.size()); + assertEquals("wrong linked filters: " + visitor.linkedFilters, 0, visitor.linkedFilters.size()); + assertEquals( + "wrong instance servlets: " + visitor.instanceServlets, 1, visitor.instanceServlets.size()); + assertEquals( + "wrong instance filters: " + visitor.instanceFilters, 1, visitor.instanceFilters.size()); + } + + public void testServletModule_badPattern() { + try { + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/%2E/*").with(new DummyServlet()); + serveRegex("/(foo|bar/").with(new DummyServlet()); + filter("/%2E/*").through(new DummyFilterImpl()); + filterRegex("/(foo|bar/").through(new DummyFilterImpl()); + } + }); + fail(); + } catch (CreationException e) { + assertEquals(4, e.getErrorMessages().size()); + } + } + + private static class Module extends ServletModule { + @Override + protected void configureServlets() { + serve("/sam/*").with(new DummyServlet()); + filter("/tara/*").through(new DummyFilterImpl()); + } + } + + private static class Visitor extends DefaultBindingTargetVisitor + implements ServletModuleTargetVisitor { + List linkedFilters = Lists.newArrayList(); + List linkedServlets = Lists.newArrayList(); + List instanceFilters = Lists.newArrayList(); + List instanceServlets = Lists.newArrayList(); + + @Override + public Void visit(LinkedFilterBinding binding) { + linkedFilters.add(binding); + return null; + } + + @Override + public Void visit(InstanceFilterBinding binding) { + instanceFilters.add(binding); + return null; + } + + @Override + public Void visit(LinkedServletBinding binding) { + linkedServlets.add(binding); + return null; + } + + @Override + public Void visit(InstanceServletBinding binding) { + instanceServlets.add(binding); + return null; + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletPipelineRequestDispatcherTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletPipelineRequestDispatcherTest.java new file mode 100644 index 0000000000..c2ba4e7dfe --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletPipelineRequestDispatcherTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.servlet.jee.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.util.Providers; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import junit.framework.TestCase; + +/** + * Tests forwarding and inclusion (RequestDispatcher actions from the servlet spec). + * + * @author Dhanji R. Prasanna (dhanji@gmail com) + */ +@SuppressWarnings("unchecked") // Safe because mock will only ever return HttpServlet +public class ServletPipelineRequestDispatcherTest extends TestCase { + private static final Key HTTP_SERLVET_KEY = Key.get(HttpServlet.class); + private static final String A_KEY = "thinglyDEgintly" + new Date() + UUID.randomUUID(); + private static final String A_VALUE = + ServletPipelineRequestDispatcherTest.class.toString() + new Date() + UUID.randomUUID(); + + public final void testIncludeManagedServlet() throws IOException, ServletException { + String pattern = "blah.html"; + final ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + + final Injector injector = createMock(Injector.class); + final Binding binding = createMock(Binding.class); + final HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getAttribute(A_KEY)).andReturn(A_VALUE); + + requestMock.setAttribute(REQUEST_DISPATCHER_REQUEST, true); + requestMock.removeAttribute(REQUEST_DISPATCHER_REQUEST); + + final boolean[] run = new boolean[1]; + final HttpServlet mockServlet = + new HttpServlet() { + @Override + protected void service( + HttpServletRequest request, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + run[0] = true; + + final Object o = request.getAttribute(A_KEY); + assertEquals("Wrong attrib returned - " + o, A_VALUE, o); + } + }; + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())).andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + expect(injector.getInstance(HTTP_SERLVET_KEY)).andReturn(mockServlet); + + final Key servetDefsKey = Key.get(TypeLiteral.get(ServletDefinition.class)); + + Binding mockBinding = createMock(Binding.class); + expect(injector.findBindingsByType(eq(servetDefsKey.getTypeLiteral()))) + .andReturn(ImmutableList.>of(mockBinding)); + Provider bindingProvider = Providers.of(servletDefinition); + expect(mockBinding.getProvider()).andReturn(bindingProvider); + + replay(injector, binding, requestMock, mockBinding); + + // Have to init the Servlet before we can dispatch to it. + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + + final RequestDispatcher dispatcher = + new ManagedServletPipeline(injector).getRequestDispatcher(pattern); + + assertNotNull(dispatcher); + dispatcher.include(requestMock, createMock(HttpServletResponse.class)); + + assertTrue("Include did not dispatch to our servlet!", run[0]); + + verify(injector, requestMock, mockBinding); + } + + public final void testForwardToManagedServlet() throws IOException, ServletException { + String pattern = "blah.html"; + final ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + + final Injector injector = createMock(Injector.class); + final Binding binding = createMock(Binding.class); + final HttpServletRequest requestMock = createMock(HttpServletRequest.class); + final HttpServletResponse mockResponse = createMock(HttpServletResponse.class); + + expect(requestMock.getAttribute(A_KEY)).andReturn(A_VALUE); + + requestMock.setAttribute(REQUEST_DISPATCHER_REQUEST, true); + requestMock.removeAttribute(REQUEST_DISPATCHER_REQUEST); + + expect(mockResponse.isCommitted()).andReturn(false); + + mockResponse.resetBuffer(); + expectLastCall().once(); + + final List paths = new ArrayList<>(); + final HttpServlet mockServlet = + new HttpServlet() { + @Override + protected void service( + HttpServletRequest request, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + paths.add(request.getRequestURI()); + + final Object o = request.getAttribute(A_KEY); + assertEquals("Wrong attrib returned - " + o, A_VALUE, o); + } + }; + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())).andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + + expect(injector.getInstance(HTTP_SERLVET_KEY)).andReturn(mockServlet); + + final Key servetDefsKey = Key.get(TypeLiteral.get(ServletDefinition.class)); + + Binding mockBinding = createMock(Binding.class); + expect(injector.findBindingsByType(eq(servetDefsKey.getTypeLiteral()))) + .andReturn(ImmutableList.>of(mockBinding)); + Provider bindingProvider = Providers.of(servletDefinition); + expect(mockBinding.getProvider()).andReturn(bindingProvider); + + replay(injector, binding, requestMock, mockResponse, mockBinding); + + // Have to init the Servlet before we can dispatch to it. + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + + final RequestDispatcher dispatcher = + new ManagedServletPipeline(injector).getRequestDispatcher(pattern); + + assertNotNull(dispatcher); + dispatcher.forward(requestMock, mockResponse); + + assertTrue("Include did not dispatch to our servlet!", paths.contains(pattern)); + + verify(injector, requestMock, mockResponse, mockBinding); + } + + public final void testForwardToManagedServletFailureOnCommittedBuffer() + throws IOException, ServletException { + IllegalStateException expected = null; + try { + forwardToManagedServletFailureOnCommittedBuffer(); + } catch (IllegalStateException ise) { + expected = ise; + } finally { + assertNotNull("Expected IllegalStateException was not thrown", expected); + } + } + + public final void forwardToManagedServletFailureOnCommittedBuffer() + throws IOException, ServletException { + String pattern = "blah.html"; + final ServletDefinition servletDefinition = + new ServletDefinition( + Key.get(HttpServlet.class), + UriPatternType.get(UriPatternType.SERVLET, pattern), + new HashMap(), + null); + + final Injector injector = createMock(Injector.class); + final Binding binding = createMock(Binding.class); + final HttpServletRequest mockRequest = createMock(HttpServletRequest.class); + final HttpServletResponse mockResponse = createMock(HttpServletResponse.class); + + expect(mockResponse.isCommitted()).andReturn(true); + + final HttpServlet mockServlet = + new HttpServlet() { + @Override + protected void service( + HttpServletRequest request, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + + final Object o = request.getAttribute(A_KEY); + assertEquals("Wrong attrib returned - " + o, A_VALUE, o); + } + }; + + expect(binding.acceptScopingVisitor((BindingScopingVisitor) anyObject())).andReturn(true); + expect(injector.getBinding(Key.get(HttpServlet.class))).andReturn(binding); + + expect(injector.getInstance(Key.get(HttpServlet.class))).andReturn(mockServlet); + + final Key servetDefsKey = Key.get(TypeLiteral.get(ServletDefinition.class)); + + Binding mockBinding = createMock(Binding.class); + expect(injector.findBindingsByType(eq(servetDefsKey.getTypeLiteral()))) + .andReturn(ImmutableList.>of(mockBinding)); + Provider bindingProvider = Providers.of(servletDefinition); + expect(mockBinding.getProvider()).andReturn(bindingProvider); + + replay(injector, binding, mockRequest, mockResponse, mockBinding); + + // Have to init the Servlet before we can dispatch to it. + servletDefinition.init(null, injector, Sets.newIdentityHashSet()); + + final RequestDispatcher dispatcher = + new ManagedServletPipeline(injector).getRequestDispatcher(pattern); + + assertNotNull(dispatcher); + + try { + dispatcher.forward(mockRequest, mockResponse); + } finally { + verify(injector, mockRequest, mockResponse, mockBinding); + } + } + + public final void testWrappedRequestUriAndUrlConsistency() { + final HttpServletRequest mockRequest = createMock(HttpServletRequest.class); + expect(mockRequest.getScheme()).andReturn("http"); + expect(mockRequest.getServerName()).andReturn("the.server"); + expect(mockRequest.getServerPort()).andReturn(12345); + replay(mockRequest); + HttpServletRequest wrappedRequest = ManagedServletPipeline.wrapRequest(mockRequest, "/new-uri"); + assertEquals("/new-uri", wrappedRequest.getRequestURI()); + assertEquals("http://the.server:12345/new-uri", wrappedRequest.getRequestURL().toString()); + } + + public final void testWrappedRequestUrlNegativePort() { + final HttpServletRequest mockRequest = createMock(HttpServletRequest.class); + expect(mockRequest.getScheme()).andReturn("http"); + expect(mockRequest.getServerName()).andReturn("the.server"); + expect(mockRequest.getServerPort()).andReturn(-1); + replay(mockRequest); + HttpServletRequest wrappedRequest = ManagedServletPipeline.wrapRequest(mockRequest, "/new-uri"); + assertEquals("/new-uri", wrappedRequest.getRequestURI()); + assertEquals("http://the.server/new-uri", wrappedRequest.getRequestURL().toString()); + } + + public final void testWrappedRequestUrlDefaultPort() { + final HttpServletRequest mockRequest = createMock(HttpServletRequest.class); + expect(mockRequest.getScheme()).andReturn("http"); + expect(mockRequest.getServerName()).andReturn("the.server"); + expect(mockRequest.getServerPort()).andReturn(80); + replay(mockRequest); + HttpServletRequest wrappedRequest = ManagedServletPipeline.wrapRequest(mockRequest, "/new-uri"); + assertEquals("/new-uri", wrappedRequest.getRequestURI()); + assertEquals("http://the.server/new-uri", wrappedRequest.getRequestURL().toString()); + } + + public final void testWrappedRequestUrlDefaultHttpsPort() { + final HttpServletRequest mockRequest = createMock(HttpServletRequest.class); + expect(mockRequest.getScheme()).andReturn("https"); + expect(mockRequest.getServerName()).andReturn("the.server"); + expect(mockRequest.getServerPort()).andReturn(443); + replay(mockRequest); + HttpServletRequest wrappedRequest = ManagedServletPipeline.wrapRequest(mockRequest, "/new-uri"); + assertEquals("/new-uri", wrappedRequest.getRequestURI()); + assertEquals("https://the.server/new-uri", wrappedRequest.getRequestURL().toString()); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletScopesTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletScopesTest.java new file mode 100644 index 0000000000..f803bc14eb --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletScopesTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.PrivateModule; +import com.google.inject.Provides; +import com.google.inject.ScopeAnnotation; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.spi.PrivateElements; +import com.google.inject.util.Providers; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; + +import jakarta.inject.Named; +import junit.framework.TestCase; + +/** + * Tests for {@link ServletScopes}. + * + * @author forster@google.com (Mike Forster) + */ +public class ServletScopesTest extends TestCase { + public void testIsRequestScopedPositive() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(Object.class, named("E")); + final Key f = Key.get(String.class, named("F")); + final Key g = Key.get(String.class, named("G")); + + Module requestScopedBindings = + new AbstractModule() { + @Override + protected void configure() { + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(ServletScopes.REQUEST); + bind(d).toProvider(Providers.of("d")).in(RequestScoped.class); + bind(e).to(AnnotatedRequestScopedClass.class); + install( + new PrivateModule() { + @Override + protected void configure() { + bind(f).toProvider(Providers.of("f")).in(RequestScoped.class); + expose(f); + } + }); + } + + @Provides + @Named("G") + @RequestScoped + String provideG() { + return "g"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(requestScopedBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + // linked bindings are not followed by modules + assertFalse(ServletScopes.isRequestScoped(map.get(a))); + assertFalse(ServletScopes.isRequestScoped(map.get(b))); + assertTrue(ServletScopes.isRequestScoped(map.get(c))); + assertTrue(ServletScopes.isRequestScoped(map.get(d))); + // annotated classes are not followed by modules + assertFalse(ServletScopes.isRequestScoped(map.get(e))); + assertTrue(ServletScopes.isRequestScoped(map.get(f))); + assertTrue(ServletScopes.isRequestScoped(map.get(g))); + + Injector injector = Guice.createInjector(requestScopedBindings, new ServletModule()); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(a))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(b))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(c))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(d))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(e))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(f))); + assertTrue(ServletScopes.isRequestScoped(injector.getBinding(g))); + } + + public void testIsRequestScopedNegative() { + final Key a = Key.get(String.class, named("A")); + final Key b = Key.get(String.class, named("B")); + final Key c = Key.get(String.class, named("C")); + final Key d = Key.get(String.class, named("D")); + final Key e = Key.get(String.class, named("E")); + final Key f = Key.get(String.class, named("F")); + final Key g = Key.get(String.class, named("G")); + final Key h = Key.get(String.class, named("H")); + final Key i = Key.get(String.class, named("I")); + final Key j = Key.get(String.class, named("J")); + + Module requestScopedBindings = + new AbstractModule() { + @Override + protected void configure() { + bind(a).to(b); + bind(b).to(c); + bind(c).toProvider(Providers.of("c")).in(Scopes.NO_SCOPE); + bind(d).toInstance("d"); + bind(e).toProvider(Providers.of("e")).asEagerSingleton(); + bind(f).toProvider(Providers.of("f")).in(Scopes.SINGLETON); + bind(g).toProvider(Providers.of("g")).in(Singleton.class); + bind(h).toProvider(Providers.of("h")).in(CustomScoped.class); + bindScope(CustomScoped.class, Scopes.NO_SCOPE); + install( + new PrivateModule() { + @Override + protected void configure() { + bind(i).toProvider(Providers.of("i")).in(CustomScoped.class); + expose(i); + } + }); + } + + @Provides + @Named("J") + @CustomScoped + String provideJ() { + return "j"; + } + }; + + @SuppressWarnings("unchecked") // we know the module contains only bindings + List moduleBindings = Elements.getElements(requestScopedBindings); + ImmutableMap, Binding> map = indexBindings(moduleBindings); + assertFalse(ServletScopes.isRequestScoped(map.get(a))); + assertFalse(ServletScopes.isRequestScoped(map.get(b))); + assertFalse(ServletScopes.isRequestScoped(map.get(c))); + assertFalse(ServletScopes.isRequestScoped(map.get(d))); + assertFalse(ServletScopes.isRequestScoped(map.get(e))); + assertFalse(ServletScopes.isRequestScoped(map.get(f))); + assertFalse(ServletScopes.isRequestScoped(map.get(g))); + assertFalse(ServletScopes.isRequestScoped(map.get(h))); + assertFalse(ServletScopes.isRequestScoped(map.get(i))); + assertFalse(ServletScopes.isRequestScoped(map.get(j))); + + Injector injector = Guice.createInjector(requestScopedBindings); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(a))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(b))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(c))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(d))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(e))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(f))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(g))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(h))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(i))); + assertFalse(ServletScopes.isRequestScoped(injector.getBinding(j))); + } + + @RequestScoped + static class AnnotatedRequestScopedClass {} + + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RUNTIME) + @ScopeAnnotation + private @interface CustomScoped {} + + private ImmutableMap, Binding> indexBindings(Iterable elements) { + ImmutableMap.Builder, Binding> builder = ImmutableMap.builder(); + for (Element element : elements) { + if (element instanceof Binding) { + Binding binding = (Binding) element; + builder.put(binding.getKey(), binding); + } else if (element instanceof PrivateElements) { + PrivateElements privateElements = (PrivateElements) element; + Map, Binding> privateBindings = indexBindings(privateElements.getElements()); + for (Key exposed : privateElements.getExposedKeys()) { + builder.put(exposed, privateBindings.get(exposed)); + } + } + } + return builder.buildOrThrow(); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletSpiVisitor.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletSpiVisitor.java new file mode 100644 index 0000000000..40fd23bf2d --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletSpiVisitor.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.Stage; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import jakarta.servlet.Filter; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import junit.framework.AssertionFailedError; + +/** + * A visitor for testing the servlet SPI extension. + * + * @author sameb@google.com (Sam Berlin) + */ +class ServletSpiVisitor extends DefaultBindingTargetVisitor + implements ServletModuleTargetVisitor { + + int otherCount = 0; + int currentCount = 0; + List actual = Lists.newArrayList(); + + /* The set of classes that are allowed to be "other" bindings. */ + Set> allowedClasses; + + ServletSpiVisitor(boolean forInjector) { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + // always ignore these things... + builder.add( + ServletRequest.class, + ServletResponse.class, + ManagedFilterPipeline.class, + ManagedServletPipeline.class, + FilterPipeline.class, + ServletContext.class, + HttpServletRequest.class, + Filter.class, + HttpServletResponse.class, + HttpSession.class, + Map.class, + HttpServlet.class, + InternalServletModule.BackwardsCompatibleServletContextProvider.class, + GuiceFilter.class); + if (forInjector) { + // only ignore these if this is for the live injector, any other time it'd be an error! + builder.add(Injector.class, Stage.class, Logger.class); + } + this.allowedClasses = builder.build(); + } + + @Override + public Integer visit(InstanceFilterBinding binding) { + actual.add(new Params(binding, binding.getFilterInstance())); + return currentCount++; + } + + @Override + public Integer visit(InstanceServletBinding binding) { + actual.add(new Params(binding, binding.getServletInstance())); + return currentCount++; + } + + @Override + public Integer visit(LinkedFilterBinding binding) { + actual.add(new Params(binding, binding.getLinkedKey())); + return currentCount++; + } + + @Override + public Integer visit(LinkedServletBinding binding) { + actual.add(new Params(binding, binding.getLinkedKey())); + return currentCount++; + } + + @Override + protected Integer visitOther(Binding binding) { + if (!allowedClasses.contains(binding.getKey().getTypeLiteral().getRawType())) { + throw new AssertionFailedError("invalid other binding: " + binding); + } + otherCount++; + return currentCount++; + } + + static class Params { + private final String pattern; + private final Object keyOrInstance; + private final Map params; + private final UriPatternType patternType; + + Params(ServletModuleBinding binding, Object keyOrInstance) { + this.pattern = binding.getPattern(); + this.keyOrInstance = keyOrInstance; + this.params = binding.getInitParams(); + this.patternType = binding.getUriPatternType(); + } + + Params( + String pattern, + Object keyOrInstance, + Map params, + UriPatternType patternType) { + this.pattern = pattern; + this.keyOrInstance = keyOrInstance; + this.params = params; + this.patternType = patternType; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Params) { + Params o = (Params) obj; + return Objects.equal(pattern, o.pattern) + && Objects.equal(keyOrInstance, o.keyOrInstance) + && Objects.equal(params, o.params) + && Objects.equal(patternType, o.patternType); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(pattern, keyOrInstance, params, patternType); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(Params.class) + .add("pattern", pattern) + .add("keyOrInstance", keyOrInstance) + .add("initParams", params) + .add("patternType", patternType) + .toString(); + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTest.java new file mode 100644 index 0000000000..beae765b09 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTest.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2022 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.jee; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.Asserts.reserialize; +import static com.google.inject.servlet.jee.ServletTestUtils.newFakeHttpServletRequest; +import static com.google.inject.servlet.jee.ServletTestUtils.newFakeHttpServletResponse; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.inject.AbstractModule; +import com.google.inject.BindingAnnotation; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.internal.Annotations; +import com.google.inject.name.Names; +import com.google.inject.servlet.jee.ServletScopes.NullObject; +import com.google.inject.util.Providers; +import java.io.IOException; +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Map; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.http.HttpSession; +import junit.framework.TestCase; + +/** @author crazybob@google.com (Bob Lee) */ +public class ServletTest extends TestCase { + private static final Key HTTP_REQ_KEY = Key.get(HttpServletRequest.class); + private static final Key HTTP_RESP_KEY = Key.get(HttpServletResponse.class); + private static final Key> REQ_PARAMS_KEY = + new Key>(RequestParameters.class) {}; + + private static final Key IN_REQUEST_NULL_KEY = Key.get(InRequest.class, Null.class); + private static final Key IN_SESSION_KEY = Key.get(InSession.class); + private static final Key IN_SESSION_NULL_KEY = Key.get(InSession.class, Null.class); + + @Override + public void setUp() { + //we need to clear the reference to the pipeline every test =( + GuiceFilter.reset(); + } + + public void testScopeExceptions() throws Exception { + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + install(new ServletModule()); + } + + @Provides + @RequestScoped + String provideString() { + return "foo"; + } + + @Provides + @SessionScoped + Integer provideInteger() { + return 1; + } + + @Provides + @RequestScoped + @Named("foo") + String provideNamedString() { + return "foo"; + } + }); + + try { + injector.getInstance(String.class); + fail(); + } catch (ProvisionException oose) { + assertContains(oose.getMessage(), "Cannot access scoped [String]."); + } + + try { + injector.getInstance(Integer.class); + fail(); + } catch (ProvisionException oose) { + assertContains(oose.getMessage(), "Cannot access scoped [Integer]."); + } + + Key key = Key.get(String.class, Names.named("foo")); + try { + injector.getInstance(key); + fail(); + } catch (ProvisionException oose) { + assertContains( + oose.getMessage(), + "Cannot access scoped [String annotated with @Named(" + + Annotations.memberValueString("value", "foo") + + ")]"); + } + } + + public void testRequestAndResponseBindings() throws Exception { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + final HttpServletResponse response = newFakeHttpServletResponse(); + + final boolean[] invoked = new boolean[1]; + GuiceFilter filter = new GuiceFilter(); + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + assertSame(request, servletRequest); + assertSame(request, injector.getInstance(ServletRequest.class)); + assertSame(request, injector.getInstance(HTTP_REQ_KEY)); + + assertSame(response, servletResponse); + assertSame(response, injector.getInstance(ServletResponse.class)); + assertSame(response, injector.getInstance(HTTP_RESP_KEY)); + + assertSame(servletRequest.getParameterMap(), injector.getInstance(REQ_PARAMS_KEY)); + } + }; + filter.doFilter(request, response, filterChain); + + assertTrue(invoked[0]); + } + + public void testRequestAndResponseBindings_wrappingFilter() throws Exception { + final HttpServletRequest request = newFakeHttpServletRequest(); + final ImmutableMap wrappedParamMap = + ImmutableMap.of("wrap", new String[] {"a", "b"}); + final HttpServletRequestWrapper requestWrapper = + new HttpServletRequestWrapper(request) { + @Override + public Map getParameterMap() { + return wrappedParamMap; + } + + @Override + public Object getAttribute(String attr) { + // Ensure that attributes are stored on the original request object. + throw new UnsupportedOperationException(); + } + }; + final HttpServletResponse response = newFakeHttpServletResponse(); + final HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response); + + final boolean[] filterInvoked = new boolean[1]; + final Injector injector = + createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + filter("/*") + .through( + new Filter() { + @Inject + Provider servletReqProvider; + @Inject Provider reqProvider; + @Inject Provider servletRespProvider; + @Inject Provider respProvider; + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter( + ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException { + filterInvoked[0] = true; + assertSame(req, servletReqProvider.get()); + assertSame(req, reqProvider.get()); + + assertSame(resp, servletRespProvider.get()); + assertSame(resp, respProvider.get()); + + chain.doFilter(requestWrapper, responseWrapper); + + assertSame(req, reqProvider.get()); + assertSame(resp, respProvider.get()); + } + + @Override + public void destroy() {} + }); + } + }); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] chainInvoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + chainInvoked[0] = true; + assertSame(requestWrapper, servletRequest); + assertSame(requestWrapper, injector.getInstance(ServletRequest.class)); + assertSame(requestWrapper, injector.getInstance(HTTP_REQ_KEY)); + + assertSame(responseWrapper, servletResponse); + assertSame(responseWrapper, injector.getInstance(ServletResponse.class)); + assertSame(responseWrapper, injector.getInstance(HTTP_RESP_KEY)); + + assertSame(servletRequest.getParameterMap(), injector.getInstance(REQ_PARAMS_KEY)); + + InRequest inRequest = injector.getInstance(InRequest.class); + assertSame(inRequest, injector.getInstance(InRequest.class)); + } + }; + filter.doFilter(request, response, filterChain); + + assertTrue(chainInvoked[0]); + assertTrue(filterInvoked[0]); + } + + public void testRequestAndResponseBindings_matchesPassedParameters() throws Exception { + final int[] filterInvoked = new int[1]; + final boolean[] servletInvoked = new boolean[1]; + createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + final HttpServletRequest[] previousReq = new HttpServletRequest[1]; + final HttpServletResponse[] previousResp = new HttpServletResponse[1]; + + final Provider servletReqProvider = getProvider(ServletRequest.class); + final Provider reqProvider = getProvider(HttpServletRequest.class); + final Provider servletRespProvider = + getProvider(ServletResponse.class); + final Provider respProvider = + getProvider(HttpServletResponse.class); + + Filter filter = + new Filter() { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException { + filterInvoked[0]++; + assertSame(req, servletReqProvider.get()); + assertSame(req, reqProvider.get()); + if (previousReq[0] != null) { + assertEquals(req, previousReq[0]); + } + + assertSame(resp, servletRespProvider.get()); + assertSame(resp, respProvider.get()); + if (previousResp[0] != null) { + assertEquals(resp, previousResp[0]); + } + + chain.doFilter( + previousReq[0] = new HttpServletRequestWrapper((HttpServletRequest) req), + previousResp[0] = + new HttpServletResponseWrapper((HttpServletResponse) resp)); + + assertSame(req, reqProvider.get()); + assertSame(resp, respProvider.get()); + } + + @Override + public void destroy() {} + }; + + filter("/*").through(filter); + filter("/*").through(filter); // filter twice to test wrapping in filters + serve("/*") + .with( + new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + servletInvoked[0] = true; + assertSame(req, servletReqProvider.get()); + assertSame(req, reqProvider.get()); + + assertSame(resp, servletRespProvider.get()); + assertSame(resp, respProvider.get()); + } + }); + } + }); + + GuiceFilter filter = new GuiceFilter(); + filter.doFilter( + newFakeHttpServletRequest(), + newFakeHttpServletResponse(), + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + throw new IllegalStateException("Shouldn't get here"); + } + }); + + assertEquals(2, filterInvoked[0]); + assertTrue(servletInvoked[0]); + } + + public void testNewRequestObject() throws CreationException, IOException, ServletException { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] invoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + assertNotNull(injector.getInstance(InRequest.class)); + assertNull(injector.getInstance(IN_REQUEST_NULL_KEY)); + } + }; + + filter.doFilter(request, null, filterChain); + + assertTrue(invoked[0]); + } + + public void testExistingRequestObject() throws CreationException, IOException, ServletException { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] invoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + + InRequest inRequest = injector.getInstance(InRequest.class); + assertSame(inRequest, injector.getInstance(InRequest.class)); + + assertNull(injector.getInstance(IN_REQUEST_NULL_KEY)); + assertNull(injector.getInstance(IN_REQUEST_NULL_KEY)); + } + }; + + filter.doFilter(request, null, filterChain); + + assertTrue(invoked[0]); + } + + public void testNewSessionObject() throws CreationException, IOException, ServletException { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] invoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + assertNotNull(injector.getInstance(InSession.class)); + assertNull(injector.getInstance(IN_SESSION_NULL_KEY)); + } + }; + + filter.doFilter(request, null, filterChain); + + assertTrue(invoked[0]); + } + + public void testExistingSessionObject() throws CreationException, IOException, ServletException { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] invoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + + InSession inSession = injector.getInstance(InSession.class); + assertSame(inSession, injector.getInstance(InSession.class)); + + assertNull(injector.getInstance(IN_SESSION_NULL_KEY)); + assertNull(injector.getInstance(IN_SESSION_NULL_KEY)); + } + }; + + filter.doFilter(request, null, filterChain); + + assertTrue(invoked[0]); + } + + public void testHttpSessionIsSerializable() throws Exception { + final Injector injector = createInjector(); + final HttpServletRequest request = newFakeHttpServletRequest(); + final HttpSession session = request.getSession(); + + GuiceFilter filter = new GuiceFilter(); + final boolean[] invoked = new boolean[1]; + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + invoked[0] = true; + assertNotNull(injector.getInstance(InSession.class)); + assertNull(injector.getInstance(IN_SESSION_NULL_KEY)); + } + }; + + filter.doFilter(request, null, filterChain); + + assertTrue(invoked[0]); + + HttpSession deserializedSession = reserialize(session); + + String inSessionKey = IN_SESSION_KEY.toString(); + String inSessionNullKey = IN_SESSION_NULL_KEY.toString(); + assertTrue(deserializedSession.getAttribute(inSessionKey) instanceof InSession); + assertEquals(NullObject.INSTANCE, deserializedSession.getAttribute(inSessionNullKey)); + } + + public void testGuiceFilterConstructors() throws Exception { + final RuntimeException servletException = new RuntimeException(); + final RuntimeException chainException = new RuntimeException(); + final Injector injector = + createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + serve("/*") + .with( + new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + throw servletException; + } + }); + } + }); + final HttpServletRequest request = newFakeHttpServletRequest(); + FilterChain filterChain = + new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + throw chainException; + } + }; + + try { + new GuiceFilter().doFilter(request, newFakeHttpServletResponse(), filterChain); + fail(); + } catch (RuntimeException e) { + assertSame(servletException, e); + } + try { + injector.getInstance(GuiceFilter.class).doFilter(request, newFakeHttpServletResponse(), filterChain); + fail(); + } catch (RuntimeException e) { + assertSame(servletException, e); + } + try { + injector + .getInstance(Key.get(GuiceFilter.class, ScopingOnly.class)) + .doFilter(request, null, filterChain); + fail(); + } catch (RuntimeException e) { + assertSame(chainException, e); + } + } + + private Injector createInjector(Module... modules) throws CreationException { + return Guice.createInjector( + Lists.asList( + new AbstractModule() { + @Override + protected void configure() { + install(new ServletModule()); + bind(InSession.class); + bind(IN_SESSION_NULL_KEY) + .toProvider(Providers.of(null)) + .in(SessionScoped.class); + bind(InRequest.class); + bind(IN_REQUEST_NULL_KEY) + .toProvider(Providers.of(null)) + .in(RequestScoped.class); + } + }, + modules)); + } + + @SessionScoped + static class InSession implements Serializable {} + + @RequestScoped + static class InRequest {} + + @BindingAnnotation + @Retention(RUNTIME) + @Target({PARAMETER, METHOD, FIELD}) + @interface Null {} +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTestUtils.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTestUtils.java new file mode 100644 index 0000000000..910b898e98 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletTestUtils.java @@ -0,0 +1,130 @@ +// Copyright 2022 Google Inc. All Rights Reserved. + +package com.google.inject.servlet.jee; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +/** + * Utilities for servlet tests. + * + * @author sameb@google.com (Sam Berlin) + */ +public class ServletTestUtils { + + private ServletTestUtils() {} + + private static class ThrowingInvocationHandler implements InvocationHandler { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + throw new UnsupportedOperationException("No methods are supported on this object"); + } + } + + /** Returns a FilterChain that does nothing. */ + public static FilterChain newNoOpFilterChain() { + return new FilterChain() { + @Override + public void doFilter(ServletRequest request, ServletResponse response) {} + }; + } + + /** Returns a fake, HttpServletRequest which stores attributes in a HashMap. */ + public static HttpServletRequest newFakeHttpServletRequest() { + HttpServletRequest delegate = + (HttpServletRequest) + Proxy.newProxyInstance( + HttpServletRequest.class.getClassLoader(), + new Class[] {HttpServletRequest.class}, + new ThrowingInvocationHandler()); + + return new HttpServletRequestWrapper(delegate) { + final Map attributes = Maps.newHashMap(); + final HttpSession session = newFakeHttpSession(); + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + @Override + public Map getParameterMap() { + return ImmutableMap.of(); + } + + @Override + public String getRequestURI() { + return "/"; + } + + @Override + public String getContextPath() { + return ""; + } + + @Override + public HttpSession getSession() { + return session; + } + }; + } + + /** + * Returns a fake, HttpServletResponse which throws an exception if any of its methods are called. + */ + public static HttpServletResponse newFakeHttpServletResponse() { + return (HttpServletResponse) + Proxy.newProxyInstance( + HttpServletResponse.class.getClassLoader(), + new Class[] {HttpServletResponse.class}, + new ThrowingInvocationHandler()); + } + + private static class FakeHttpSessionHandler implements InvocationHandler, Serializable { + final Map attributes = Maps.newHashMap(); + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String name = method.getName(); + if ("setAttribute".equals(name)) { + attributes.put((String) args[0], args[1]); + return null; + } else if ("getAttribute".equals(name)) { + return attributes.get(args[0]); + } else { + throw new UnsupportedOperationException(); + } + } + } + + /** Returns a fake, serializable HttpSession which stores attributes in a HashMap. */ + public static HttpSession newFakeHttpSession() { + return (HttpSession) + Proxy.newProxyInstance( + HttpSession.class.getClassLoader(), + new Class[] {HttpSession.class}, + new FakeHttpSessionHandler()); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletUtilsTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletUtilsTest.java new file mode 100644 index 0000000000..33a8377055 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/ServletUtilsTest.java @@ -0,0 +1,70 @@ +// Copyright 2022 Google Inc. All Rights Reserved. + +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * Unit test for the servlet utility class. + * + * @author ntang@google.com (Michael Tang) + */ +public class ServletUtilsTest extends TestCase { + public void testGetContextRelativePath() { + assertEquals( + "/test.html", getContextRelativePath("/a_context_path", "/a_context_path/test.html")); + assertEquals("/test.html", getContextRelativePath("", "/test.html")); + assertEquals("/test.html", getContextRelativePath("", "/foo/../test.html")); + assertEquals("/test.html", getContextRelativePath("", "/././foo/../test.html")); + assertEquals("/test.html", getContextRelativePath("", "/foo/../../../../test.html")); + assertEquals("/test.html", getContextRelativePath("", "/foo/%2E%2E/test.html")); + // %2E == '.' + assertEquals("/test.html", getContextRelativePath("", "/foo/%2E%2E/test.html")); + // %2F == '/' + assertEquals("/foo/%2F/test.html", getContextRelativePath("", "/foo/%2F/test.html")); + // %66 == 'f' + assertEquals("/foo.html", getContextRelativePath("", "/%66oo.html")); + } + + public void testGetContextRelativePath_preserveQuery() { + assertEquals("/foo?q=f", getContextRelativePath("", "/foo?q=f")); + assertEquals("/foo?q=%20+%20", getContextRelativePath("", "/foo?q=%20+%20")); + } + + public void testGetContextRelativePathWithWrongPath() { + assertNull(getContextRelativePath("/a_context_path", "/test.html")); + } + + public void testGetContextRelativePathWithRootPath() { + assertEquals("/", getContextRelativePath("/a_context_path", "/a_context_path")); + } + + public void testGetContextRelativePathWithEmptyPath() { + assertNull(getContextRelativePath("", "")); + } + + public void testNormalizePath() { + assertEquals("foobar", ServletUtils.normalizePath("foobar")); + assertEquals("foo+bar", ServletUtils.normalizePath("foo+bar")); + assertEquals("foo%20bar", ServletUtils.normalizePath("foo bar")); + assertEquals("foo%25-bar", ServletUtils.normalizePath("foo%-bar")); + assertEquals("foo%25+bar", ServletUtils.normalizePath("foo%+bar")); + assertEquals("foo%25-0bar", ServletUtils.normalizePath("foo%-0bar")); + } + + private String getContextRelativePath(String contextPath, String requestPath) { + HttpServletRequest mock = createMock(HttpServletRequest.class); + expect(mock.getContextPath()).andReturn(contextPath); + expect(mock.getRequestURI()).andReturn(requestPath); + replay(mock); + String contextRelativePath = ServletUtils.getContextRelativePath(mock); + verify(mock); + return contextRelativePath; + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/TransferRequestIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/TransferRequestIntegrationTest.java new file mode 100644 index 0000000000..1c05f48028 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/TransferRequestIntegrationTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2022 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.jee; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provides; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.TestCase; + +// TODO: Add test for HTTP transferring. +/** Tests transferring of entire request scope. */ + +public class TransferRequestIntegrationTest extends TestCase { + + public void testTransferHttp_outOfScope() { + try { + ServletScopes.transferRequest(() -> false); + fail(); + } catch (OutOfScopeException expected) { + } + } + + public void testTransferNonHttp_outOfScope() { + try { + ServletScopes.transferRequest(() -> false); + fail(); + } catch (OutOfScopeException expected) { + } + } + + public void testTransferNonHttp_outOfScope_closeable() { + try { + ServletScopes.transferRequest(); + fail(); + } catch (OutOfScopeException expected) { + } + } + + public void testTransferNonHttpRequest() throws Exception { + final Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(RequestScoped.class, ServletScopes.REQUEST); + } + + @Provides + @RequestScoped + Object provideObject() { + return new Object(); + } + }); + + Callable> callable = + () -> { + final Object original = injector.getInstance(Object.class); + return ServletScopes.transferRequest( + () -> original == injector.getInstance(Object.class)); + }; + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + Callable transfer = ServletScopes.scopeRequest(callable, seedMap).call(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + assertTrue(executor.submit(transfer).get()); + executor.shutdownNow(); + } + + public void testTransferNonHttpRequest_closeable() throws Exception { + final Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(RequestScoped.class, ServletScopes.REQUEST); + } + + @Provides + @RequestScoped + Object provideObject() { + return new Object(); + } + }); + + class Data { + Object object; + RequestScoper scoper; + } + + Callable callable = + () -> { + Data data = new Data(); + data.object = injector.getInstance(Object.class); + data.scoper = ServletScopes.transferRequest(); + return data; + }; + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + Data data = ServletScopes.scopeRequest(callable, seedMap).call(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + RequestScoper.CloseableScope scope = data.scoper.open(); + try { + assertSame(data.object, injector.getInstance(Object.class)); + } finally { + scope.close(); + executor.shutdownNow(); + } + } + + public void testTransferNonHttpRequest_concurrentUseBlocks() throws Exception { + Callable callable = + () -> { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future future = executor.submit(ServletScopes.transferRequest(() -> false)); + try { + return future.get(100, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + return true; + } + } finally { + executor.shutdownNow(); + } + }; + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + assertTrue(ServletScopes.scopeRequest(callable, seedMap).call()); + } + + public void testTransferNonHttpRequest_concurrentUseBlocks_closeable() throws Exception { + Callable callable = + () -> { + final RequestScoper scoper = ServletScopes.transferRequest(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future future = + executor.submit( + () -> { + RequestScoper.CloseableScope scope = scoper.open(); + try { + return false; + } finally { + scope.close(); + } + }); + try { + return future.get(100, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + return true; + } + } finally { + executor.shutdownNow(); + } + }; + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + assertTrue(ServletScopes.scopeRequest(callable, seedMap).call()); + } + + public void testTransferNonHttpRequest_concurrentUseSameThreadOk() throws Exception { + Callable callable = () -> ServletScopes.transferRequest(() -> false).call(); + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + assertFalse(ServletScopes.scopeRequest(callable, seedMap).call()); + } + + public void testTransferNonHttpRequest_concurrentUseSameThreadOk_closeable() throws Exception { + Callable callable = + () -> { + RequestScoper.CloseableScope scope = ServletScopes.transferRequest().open(); + try { + return false; + } finally { + scope.close(); + } + }; + + ImmutableMap, Object> seedMap = ImmutableMap.of(); + assertFalse(ServletScopes.scopeRequest(callable, seedMap).call()); + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/UriPatternTypeTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/UriPatternTypeTest.java new file mode 100644 index 0000000000..0467b6646e --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/UriPatternTypeTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 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.jee; + +import static junit.framework.Assert.fail; + +import junit.framework.TestCase; + +public class UriPatternTypeTest extends TestCase { + + public void testMatches_servlet() { + UriPatternMatcher pattern = UriPatternType.get(UriPatternType.SERVLET, "/foo/*"); + assertTrue(pattern.matches("/foo/asdf")); + assertTrue(pattern.matches("/foo/asdf?val=1")); + assertFalse(pattern.matches("/path/file.bar")); + assertFalse(pattern.matches("/path/file.bar?val=1")); + assertFalse(pattern.matches("/asdf")); + assertFalse(pattern.matches("/asdf?val=1")); + + pattern = UriPatternType.get(UriPatternType.SERVLET, "*.bar"); + assertFalse(pattern.matches("/foo/asdf")); + assertFalse(pattern.matches("/foo/asdf?val=1")); + assertTrue(pattern.matches("/path/file.bar")); + assertTrue(pattern.matches("/path/file.bar?val=1")); + assertFalse(pattern.matches("/asdf")); + assertFalse(pattern.matches("/asdf?val=1")); + + pattern = UriPatternType.get(UriPatternType.SERVLET, "/asdf"); + assertFalse(pattern.matches("/foo/asdf")); + assertFalse(pattern.matches("/foo/asdf?val=1")); + assertFalse(pattern.matches("/path/file.bar")); + assertFalse(pattern.matches("/path/file.bar?val=1")); + assertTrue(pattern.matches("/asdf")); + assertTrue(pattern.matches("/asdf?val=1")); + } + + public void testMatches_regex() { + UriPatternMatcher pattern = UriPatternType.get(UriPatternType.REGEX, "/.*/foo"); + assertFalse(pattern.matches("/foo/asdf")); + assertFalse(pattern.matches("/foo/asdf?val=1")); + assertTrue(pattern.matches("/path/foo")); + assertTrue(pattern.matches("/path/foo?val=1")); + assertFalse(pattern.matches("/foo")); + assertFalse(pattern.matches("/foo?val=1")); + } + + public void testPatternWithPercentEncodedChars_servlet() { + try { + UriPatternType.get(UriPatternType.SERVLET, "/foo/%2f/*"); + fail(); + } catch (IllegalArgumentException iae) { + assertTrue(iae.getMessage().contains("Servlet patterns cannot contain escape patterns.")); + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsFilterDispatchIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsFilterDispatchIntegrationTest.java new file mode 100644 index 0000000000..7fed324d9f --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsFilterDispatchIntegrationTest.java @@ -0,0 +1,189 @@ +package com.google.inject.servlet.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * This tests that filter stage of the pipeline dispatches correctly to guice-managed filters. + * + *

WARNING(dhanji): Non-parallelizable test =( + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class VarargsFilterDispatchIntegrationTest extends TestCase { + private static int inits, doFilters, destroys; + + @Override + public final void setUp() { + inits = 0; + doFilters = 0; + destroys = 0; + + GuiceFilter.reset(); + } + + public final void testDispatchRequestToManagedPipeline() throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + // This is actually a double match for "/*" + filter("/*", "*.html", "/*").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not" + + " fire correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 3 && destroys == 1); + } + + public final void testDispatchThatNoFiltersFire() throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/public/*", "*.html", "*.xml").through(Key.get(TestFilter.class)); + + // These filters should never fire + filter("/index/*").through(Key.get(TestFilter.class)); + filter("*.jsp").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.xhtml").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not " + + "fire correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 0 && destroys == 1); + } + + public final void testDispatchFilterPipelineWithRegexMatching() + throws ServletException, IOException { + + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filterRegex("/[A-Za-z]*", "/index").through(TestFilter.class); + + //these filters should never fire + filterRegex("\\w").through(Key.get(TestFilter.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index").anyTimes(); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire " + + "correct number of times-- inits: " + + inits + + "; dos: " + + doFilters + + "; destroys: " + + destroys, + inits == 1 && doFilters == 2 && destroys == 1); + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + doFilters++; + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroys++; + } + } +} diff --git a/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsServletDispatchIntegrationTest.java b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsServletDispatchIntegrationTest.java new file mode 100644 index 0000000000..c34d18aba9 --- /dev/null +++ b/extensions/jakarta-servlet/test/com/google/inject/servlet/jee/VarargsServletDispatchIntegrationTest.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2022 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.jee; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import java.io.IOException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import junit.framework.TestCase; + +/** + * Tests the FilterPipeline that dispatches to guice-managed servlets, is a full integration test, + * with a real injector. + * + * @author Dhanji R. Prasanna (dhanji gmail com) + */ +public class VarargsServletDispatchIntegrationTest extends TestCase { + private static int inits, services, destroys, doFilters; + + @Override + public void setUp() { + inits = 0; + services = 0; + destroys = 0; + doFilters = 0; + + GuiceFilter.reset(); + } + + public final void testDispatchRequestToManagedPipelineServlets() + throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + serve("/*", "/index.html").with(TestServlet.class); + + // These servets should never fire... (ordering test) + serve("*.html", "/o/*", "/index/*", "*.jsp").with(Key.get(NeverServlet.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").times(1); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire correct number of times-- inits: " + + inits + + "; dos: " + + services + + "; destroys: " + + destroys, + inits == 2 && services == 1 && destroys == 2); + } + + public final void testVarargsSkipDispatchRequestToManagedPipelineServlets() + throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + serve("/notindex", "/&*", "/index.html").with(TestServlet.class); + + // These servets should never fire... (ordering test) + serve("*.html", "/*", "/index/*", "*.jsp").with(Key.get(NeverServlet.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").times(3); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire correct number of times-- inits: " + + inits + + "; dos: " + + services + + "; destroys: " + + destroys, + inits == 2 && services == 1 && destroys == 2); + } + + public final void testDispatchRequestToManagedPipelineWithFilter() + throws ServletException, IOException { + final Injector injector = + Guice.createInjector( + new ServletModule() { + + @Override + protected void configureServlets() { + filter("/*").through(TestFilter.class); + + serve("/*").with(TestServlet.class); + + // These servets should never fire... + serve("*.html", "/y/*", "/index/*", "*.jsp").with(Key.get(NeverServlet.class)); + } + }); + + final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class); + + pipeline.initPipeline(null); + + //create ourselves a mock request with test URI + HttpServletRequest requestMock = createMock(HttpServletRequest.class); + + expect(requestMock.getRequestURI()).andReturn("/index.html").times(2); + expect(requestMock.getContextPath()).andReturn("").anyTimes(); + + //dispatch request + replay(requestMock); + + pipeline.dispatch(requestMock, null, createMock(FilterChain.class)); + + pipeline.destroyPipeline(); + + verify(requestMock); + + assertTrue( + "lifecycle states did not fire correct number of times-- inits: " + + inits + + "; dos: " + + services + + "; destroys: " + + destroys, + inits == 3 && services == 1 && destroys == 3 && doFilters == 1); + } + + @Singleton + public static class TestServlet extends HttpServlet { + @Override + public void init(ServletConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + services++; + } + + @Override + public void destroy() { + destroys++; + } + } + + @Singleton + public static class NeverServlet extends HttpServlet { + @Override + public void init(ServletConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + fail("NeverServlet was fired, when it should not have been."); + } + + @Override + public void destroy() { + destroys++; + } + } + + @Singleton + public static class TestFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + inits++; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + doFilters++; + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + destroys++; + } + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 885bc2f9e2..d50388c8ad 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -20,6 +20,7 @@ assistedinject dagger-adapter grapher + jakarta-servlet jmx jndi persist