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.stream.Collectors;
30  import java.util.stream.Stream;
31  
32  import com.google.inject.Key;
33  import com.google.inject.OutOfScopeException;
34  import com.google.inject.Provider;
35  import com.google.inject.Scope;
36  
37  /**
38   * SessionScope
39   */
40  public class SessionScope implements Scope {
41  
42      private static final Provider<Object> SEEDED_KEY_PROVIDER = () -> {
43          throw new IllegalStateException();
44      };
45  
46      /**
47       * ScopeState
48       */
49      protected static final class ScopeState {
50          private final Map<Key<?>, CachingProvider<?>> provided = new ConcurrentHashMap<>();
51  
52          public <T> void seed(Class<T> clazz, Provider<T> value) {
53              provided.put(Key.get(clazz), new CachingProvider<>(value));
54          }
55  
56          @SuppressWarnings("unchecked")
57          public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) {
58              Provider<?> provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped));
59              return (Provider<T>) provider;
60          }
61  
62          public Collection<CachingProvider<?>> providers() {
63              return provided.values();
64          }
65      }
66  
67      private final List<ScopeState> values = new CopyOnWriteArrayList<>();
68  
69      public void enter() {
70          values.add(0, new ScopeState());
71      }
72  
73      protected ScopeState getScopeState() {
74          if (values.isEmpty()) {
75              throw new OutOfScopeException("Cannot access session scope outside of a scoping block");
76          }
77          return values.get(0);
78      }
79  
80      public void exit() {
81          if (values.isEmpty()) {
82              throw new IllegalStateException();
83          }
84          values.remove(0);
85      }
86  
87      public <T> void seed(Class<T> clazz, Provider<T> value) {
88          getScopeState().seed(clazz, value);
89      }
90  
91      public <T> void seed(Class<T> clazz, final T value) {
92          seed(clazz, (Provider<T>) () -> value);
93      }
94  
95      public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
96          // Lazy evaluating provider
97          return () -> {
98              if (values.isEmpty()) {
99                  return createProxy(key, unscoped);
100             } else {
101                 return getScopeState().scope(key, unscoped).get();
102             }
103         };
104     }
105 
106     @SuppressWarnings("unchecked")
107     private <T> T createProxy(Key<T> key, Provider<T> unscoped) {
108         InvocationHandler dispatcher = (proxy, method, args) -> {
109             method.setAccessible(true);
110             try {
111                 return method.invoke(getScopeState().scope(key, unscoped).get(), args);
112             } catch (InvocationTargetException e) {
113                 throw e.getCause();
114             }
115         };
116         Class<T> superType = (Class<T>) key.getTypeLiteral().getRawType();
117         Class<?>[] interfaces = getInterfaces(superType);
118         return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher);
119     }
120 
121     private Class<?>[] getInterfaces(Class<?> superType) {
122         if (superType.isInterface()) {
123             return new Class<?>[] {superType};
124         } else {
125             for (Annotation a : superType.getAnnotations()) {
126                 Class<? extends Annotation> annotationType = a.annotationType();
127                 if ("org.eclipse.sisu.Typed".equals(annotationType.getName())
128                         || "javax.enterprise.inject.Typed".equals(annotationType.getName())
129                         || "jakarta.enterprise.inject.Typed".equals(annotationType.getName())) {
130                     try {
131                         Class<?>[] value =
132                                 (Class<?>[]) annotationType.getMethod("value").invoke(a);
133                         if (value.length == 0) {
134                             value = superType.getInterfaces();
135                         }
136                         List<Class<?>> nonInterfaces =
137                                 Stream.of(value).filter(c -> !c.isInterface()).collect(Collectors.toList());
138                         if (!nonInterfaces.isEmpty()) {
139                             throw new IllegalArgumentException(
140                                     "The Typed annotation must contain only interfaces but the following types are not: "
141                                             + nonInterfaces);
142                         }
143                         return value;
144                     } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
145                         throw new IllegalStateException(e);
146                     }
147                 }
148             }
149             throw new IllegalArgumentException("The use of session scoped proxies require "
150                     + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation");
151         }
152     }
153 
154     /**
155      * A provider wrapping an existing provider with a cache
156      * @param <T> the provided type
157      */
158     protected static class CachingProvider<T> implements Provider<T> {
159         private final Provider<T> provider;
160         private volatile T value;
161 
162         CachingProvider(Provider<T> provider) {
163             this.provider = provider;
164         }
165 
166         public T value() {
167             return value;
168         }
169 
170         @Override
171         public T get() {
172             if (value == null) {
173                 synchronized (this) {
174                     if (value == null) {
175                         value = provider.get();
176                     }
177                 }
178             }
179             return value;
180         }
181     }
182 
183     @SuppressWarnings({"unchecked"})
184     public static <T> Provider<T> seededKeyProvider() {
185         return (Provider<T>) SEEDED_KEY_PROVIDER;
186     }
187 
188     public static <T> Provider<T> seededKeyProvider(Class<? extends T> clazz) {
189         return () -> {
190             throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope.");
191         };
192     }
193 }