1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
156
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 }