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.impl.di;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.InvocationHandler;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.CopyOnWriteArrayList;
30  import java.util.function.Supplier;
31  import java.util.stream.Stream;
32  
33  import org.apache.maven.api.annotations.Nonnull;
34  import org.apache.maven.di.Key;
35  import org.apache.maven.di.Scope;
36  import org.apache.maven.di.impl.Types;
37  
38  public class SessionScope implements Scope {
39  
40      /**
41       * ScopeState
42       */
43      protected static final class ScopeState {
44          private final Map<Key<?>, CachingProvider<?>> provided = new ConcurrentHashMap<>();
45  
46          public <T> void seed(Class<T> clazz, Supplier<T> value) {
47              provided.put(Key.of(clazz), new CachingProvider<>(value));
48          }
49  
50          @SuppressWarnings("unchecked")
51          public <T> Supplier<T> scope(Key<T> key, Supplier<T> unscoped) {
52              Supplier<?> provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped));
53              return (Supplier<T>) provider;
54          }
55  
56          public Collection<CachingProvider<?>> providers() {
57              return provided.values();
58          }
59      }
60  
61      protected final List<ScopeState> values = new CopyOnWriteArrayList<>();
62  
63      public void enter() {
64          values.add(0, new ScopeState());
65      }
66  
67      protected ScopeState getScopeState() {
68          if (values.isEmpty()) {
69              throw new OutOfScopeException("Cannot access session scope outside of a scoping block");
70          }
71          return values.get(0);
72      }
73  
74      public void exit() {
75          if (values.isEmpty()) {
76              throw new IllegalStateException();
77          }
78          values.remove(0);
79      }
80  
81      public <T> void seed(Class<T> clazz, Supplier<T> value) {
82          getScopeState().seed(clazz, value);
83      }
84  
85      public <T> void seed(Class<T> clazz, T value) {
86          seed(clazz, (Supplier<T>) () -> value);
87      }
88  
89      @Nonnull
90      @Override
91      public <T> Supplier<T> scope(@Nonnull Key<T> key, @Nonnull Supplier<T> unscoped) {
92          // Lazy evaluating provider
93          return () -> {
94              if (values.isEmpty()) {
95                  return createProxy(key, unscoped);
96              } else {
97                  return getScopeState().scope(key, unscoped).get();
98              }
99          };
100     }
101 
102     @SuppressWarnings("unchecked")
103     protected <T> T createProxy(Key<T> key, Supplier<T> unscoped) {
104         InvocationHandler dispatcher = (proxy, method, args) -> dispatch(key, unscoped, method, args);
105         Class<T> superType = (Class<T>) Types.getRawType(key.getType());
106         Class<?>[] interfaces = getInterfaces(superType);
107         return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher);
108     }
109 
110     protected <T> Object dispatch(Key<T> key, Supplier<T> unscoped, Method method, Object[] args) throws Throwable {
111         method.setAccessible(true);
112         try {
113             return method.invoke(getScopeState().scope(key, unscoped).get(), args);
114         } catch (InvocationTargetException e) {
115             throw e.getCause();
116         }
117     }
118 
119     protected Class<?>[] getInterfaces(Class<?> superType) {
120         if (superType.isInterface()) {
121             return new Class<?>[] {superType};
122         } else {
123             for (Annotation a : superType.getAnnotations()) {
124                 Class<? extends Annotation> annotationType = a.annotationType();
125                 if (isTypeAnnotation(annotationType)) {
126                     try {
127                         Class<?>[] value =
128                                 (Class<?>[]) annotationType.getMethod("value").invoke(a);
129                         if (value.length == 0) {
130                             value = superType.getInterfaces();
131                         }
132                         List<Class<?>> nonInterfaces =
133                                 Stream.of(value).filter(c -> !c.isInterface()).toList();
134                         if (!nonInterfaces.isEmpty()) {
135                             throw new IllegalArgumentException(
136                                     "The Typed annotation must contain only interfaces but the following types are not: "
137                                             + nonInterfaces);
138                         }
139                         return value;
140                     } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
141                         throw new IllegalStateException(e);
142                     }
143                 }
144             }
145             throw new IllegalArgumentException("The use of session scoped proxies require "
146                     + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation");
147         }
148     }
149 
150     protected boolean isTypeAnnotation(Class<? extends Annotation> annotationType) {
151         return "org.apache.maven.api.di.Typed".equals(annotationType.getName());
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 Supplier<T> {
159         private final Supplier<T> provider;
160         private volatile T value;
161 
162         CachingProvider(Supplier<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     public static <T> Supplier<T> seededKeySupplier(Class<? extends T> clazz) {
184         return () -> {
185             throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope.");
186         };
187     }
188 }