diff --git a/maven-core/pom.xml b/maven-core/pom.xml index eac040fd51..dda9f8be42 100644 --- a/maven-core/pom.xml +++ b/maven-core/pom.xml @@ -174,6 +174,12 @@ under the License. plexus-testing test + + org.mockito + mockito-junit-jupiter + ${mockitoVersion} + test + diff --git a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java index 320ab1a3a8..aa8e45116b 100644 --- a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java +++ b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java @@ -18,11 +18,16 @@ */ package org.apache.maven.session.scope.internal; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.inject.Key; import com.google.inject.OutOfScopeException; @@ -89,7 +94,47 @@ public class SessionScope implements Scope { public Provider scope(final Key key, final Provider unscoped) { // Lazy evaluating provider - return () -> getScopeState().scope(key, unscoped).get(); + return () -> { + if (values.isEmpty()) { + return createProxy(key, unscoped); + } else { + return getScopeState().scope(key, unscoped).get(); + } + }; + } + + @SuppressWarnings("unchecked") + private T createProxy(Key key, Provider unscoped) { + InvocationHandler dispatcher = (proxy, method, args) -> { + method.setAccessible(true); + return method.invoke(getScopeState().scope(key, unscoped).get(), args); + }; + Class superType = (Class) key.getTypeLiteral().getRawType(); + for (Annotation a : superType.getAnnotations()) { + Class annotationType = a.annotationType(); + if ("org.eclipse.sisu.Typed".equals(annotationType.getName()) + || "javax.enterprise.inject.Typed".equals(annotationType.getName())) { + try { + Class[] value = + (Class[]) annotationType.getMethod("value").invoke(a); + if (value.length == 0) { + value = superType.getInterfaces(); + } + List> nonInterfaces = + Stream.of(value).filter(c -> !c.isInterface()).collect(Collectors.toList()); + if (!nonInterfaces.isEmpty()) { + throw new IllegalArgumentException( + "The Typed annotation must contain only interfaces but the following types are not: " + + nonInterfaces); + } + return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), value, dispatcher); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + throw new IllegalArgumentException("The use of session scoped proxies require " + + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation"); } /** diff --git a/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java new file mode 100644 index 0000000000..5988c64c9e --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.session.scope; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import com.google.inject.OutOfScopeException; +import org.apache.maven.SessionScoped; +import org.apache.maven.api.Session; +import org.apache.maven.session.scope.internal.SessionScope; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.codehaus.plexus.testing.PlexusTest; +import org.eclipse.sisu.Typed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@PlexusTest +@ExtendWith(MockitoExtension.class) +public class SessionScopeProxyTest { + + @Mock + Session session; + + @Inject + SessionScope sessionScope; + + @Inject + PlexusContainer container; + + @Test + void testProxiedSessionScopedBean() throws ComponentLookupException { + ComponentLookupException e = + assertThrows(ComponentLookupException.class, () -> container.lookup(MySingletonBean2.class)); + assertTrue(e.getMessage().matches("[\\s\\S]*: Can not set .* field .* to [\\s\\S]*")); + + MySingletonBean bean = container.lookup(MySingletonBean.class); + assertNotNull(bean); + assertNotNull(bean.anotherBean); + assertSame(bean.anotherBean.getClass(), AnotherBean.class); + assertNotNull(bean.myBean); + assertNotSame(bean.myBean.getClass(), MySessionScopedBean.class); + + assertThrows(OutOfScopeException.class, () -> bean.myBean.getSession()); + + sessionScope.enter(); + sessionScope.seed(Session.class, this.session); + assertNotNull(bean.myBean.getSession()); + assertNotNull(bean.myBean.getAnotherBean()); + assertSame(bean.myBean.getAnotherBean().getClass(), AnotherBean.class); + } + + @Named + static class MySingletonBean { + @Inject + @Named("scoped") + BeanItf myBean; + + @Inject + @Named("another") + BeanItf2 anotherBean; + } + + @Named + static class MySingletonBean2 { + @Inject + @Named("scoped") + MySessionScopedBean myBean; + + @Inject + @Named("another") + BeanItf2 anotherBean; + } + + interface BeanItf { + Session getSession(); + + BeanItf2 getAnotherBean(); + } + + interface BeanItf2 {} + + @Named("another") + @Singleton + static class AnotherBean implements BeanItf2 {} + + @Named("scoped") + @SessionScoped + @Typed + static class MySessionScopedBean implements BeanItf { + @Inject + Session session; + + @Inject + BeanItf2 anotherBean; + + public Session getSession() { + return session; + } + + public BeanItf2 getAnotherBean() { + return anotherBean; + } + } +}