View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.session.scope.internal;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.InvocationHandler;
23  import java.lang.reflect.InvocationTargetException;
24  import java.util.Collection;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.CopyOnWriteArrayList;
29  import java.util.function.Supplier;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import com.google.inject.Key;
34  import com.google.inject.OutOfScopeException;
35  import com.google.inject.Provider;
36  import com.google.inject.Scope;
37  import com.google.inject.name.Names;
38  
39  /**
40   * SessionScope
41   */
42  public class SessionScope implements Scope, org.apache.maven.di.Scope {
43  
44      private static final Provider<Object> SEEDED_KEY_PROVIDER = () -> {
45          throw new IllegalStateException();
46      };
47  
48      /**
49       * ScopeState
50       */
51      protected static final class ScopeState {
52          private final Map<Key<?>, CachingProvider<?>> provided = new ConcurrentHashMap<>();
53  
54          public <T> void seed(Class<T> clazz, Provider<T> value) {
55              provided.put(Key.get(clazz), new CachingProvider<>(value));
56          }
57  
58          @SuppressWarnings("unchecked")
59          public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
60              Provider<?> provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped));
61              return (Provider<T>) provider;
62          }
63  
64          public Collection<CachingProvider<?>> providers() {
65              return provided.values();
66          }
67      }
68  
69      private final List<ScopeState> values = new CopyOnWriteArrayList<>();
70  
71      public void enter() {
72          values.add(0, new ScopeState());
73      }
74  
75      protected ScopeState getScopeState() {
76          if (values.isEmpty()) {
77              throw new OutOfScopeException("Cannot access session scope outside of a scoping block");
78          }
79          return values.get(0);
80      }
81  
82      public void exit() {
83          if (values.isEmpty()) {
84              throw new IllegalStateException();
85          }
86          values.remove(0);
87      }
88  
89      public <T> void seed(Class<T> clazz, Provider<T> value) {
90          getScopeState().seed(clazz, value);
91      }
92  
93      public <T> void seed(Class<T> clazz, final T value) {
94          seed(clazz, (Provider<T>) () -> value);
95      }
96  
97      public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
98          // Lazy evaluating provider
99          return () -> {
100             if (values.isEmpty()) {
101                 return createProxy(key, unscoped);
102             } else {
103                 return getScopeState().scope(key, unscoped).get();
104             }
105         };
106     }
107 
108     @SuppressWarnings("unchecked")
109     @Override
110     public <T> Supplier<T> scope(org.apache.maven.di.Key<T> key, Annotation scope, Supplier<T> unscoped) {
111         Object qualifier = key.getQualifier();
112         Key<?> k = qualifier != null
113                 ? Key.get(key.getType(), qualifier instanceof String s ? Names.named(s) : (Annotation) qualifier)
114                 : Key.get(key.getType());
115         Provider<T> up = unscoped::get;
116         Provider<T> p = scope((Key<T>) k, up);
117         return p::get;
118     }
119 
120     @SuppressWarnings("unchecked")
121     private <T> T createProxy(Key<T> key, Provider<T> unscoped) {
122         InvocationHandler dispatcher = (proxy, method, args) -> {
123             method.setAccessible(true);
124             try {
125                 return method.invoke(getScopeState().scope(key, unscoped).get(), args);
126             } catch (InvocationTargetException e) {
127                 throw e.getCause();
128             }
129         };
130         Class<T> superType = (Class<T>) key.getTypeLiteral().getRawType();
131         Class<?>[] interfaces = getInterfaces(superType);
132         return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher);
133     }
134 
135     private Class<?>[] getInterfaces(Class<?> superType) {
136         if (superType.isInterface()) {
137             return new Class<?>[] {superType};
138         } else {
139             for (Annotation a : superType.getAnnotations()) {
140                 Class<? extends Annotation> annotationType = a.annotationType();
141                 if ("org.apache.maven.api.di.Typed".equals(annotationType.getName())
142                         || "org.eclipse.sisu.Typed".equals(annotationType.getName())
143                         || "javax.enterprise.inject.Typed".equals(annotationType.getName())
144                         || "jakarta.enterprise.inject.Typed".equals(annotationType.getName())) {
145                     try {
146                         Class<?>[] value =
147                                 (Class<?>[]) annotationType.getMethod("value").invoke(a);
148                         if (value.length == 0) {
149                             value = superType.getInterfaces();
150                         }
151                         List<Class<?>> nonInterfaces =
152                                 Stream.of(value).filter(c -> !c.isInterface()).collect(Collectors.toList());
153                         if (!nonInterfaces.isEmpty()) {
154                             throw new IllegalArgumentException(
155                                     "The Typed annotation must contain only interfaces but the following types are not: "
156                                             + nonInterfaces);
157                         }
158                         return value;
159                     } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
160                         throw new IllegalStateException(e);
161                     }
162                 }
163             }
164             throw new IllegalArgumentException("The use of session scoped proxies require "
165                     + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation");
166         }
167     }
168 
169     /**
170      * A provider wrapping an existing provider with a cache
171      * @param <T> the provided type
172      */
173     protected static class CachingProvider<T> implements Provider<T> {
174         private final Provider<T> provider;
175         private volatile T value;
176 
177         CachingProvider(Provider<T> provider) {
178             this.provider = provider;
179         }
180 
181         public T value() {
182             return value;
183         }
184 
185         @Override
186         public T get() {
187             if (value == null) {
188                 synchronized (this) {
189                     if (value == null) {
190                         value = provider.get();
191                     }
192                 }
193             }
194             return value;
195         }
196     }
197 
198     @SuppressWarnings({"unchecked"})
199     public static <T> Provider<T> seededKeyProvider() {
200         return (Provider<T>) SEEDED_KEY_PROVIDER;
201     }
202 
203     public static <T> Provider<T> seededKeyProvider(Class<? extends T> clazz) {
204         return () -> {
205             throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope.");
206         };
207     }
208 }