contextual() {
30 | return contextual;
31 | }
32 |
33 | int index() {
34 | return index;
35 | }
36 |
37 | @Override
38 | public boolean equals(Object o) {
39 | if (this == o) {
40 | return true;
41 | }
42 | if (o == null || getClass() != o.getClass()) {
43 | return false;
44 | }
45 | PoolKey poolKey = (PoolKey) o;
46 | return index == poolKey.index &&
47 | Objects.equals(contextual, poolKey.contextual);
48 | }
49 |
50 | @Override
51 | public int hashCode() {
52 | return Objects.hash(contextual, index);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/PoolLockTimeoutException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | public class PoolLockTimeoutException extends RuntimeException {
16 |
17 | public PoolLockTimeoutException() {
18 | super();
19 | }
20 |
21 | public PoolLockTimeoutException(String message) {
22 | super(message);
23 | }
24 |
25 | public PoolLockTimeoutException(String message, Throwable cause) {
26 | super(message, cause);
27 | }
28 |
29 | public PoolLockTimeoutException(Throwable cause) {
30 | super(cause);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/Pooled.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | import static java.lang.annotation.ElementType.METHOD;
16 | import static java.lang.annotation.ElementType.TYPE;
17 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
18 | import static java.util.concurrent.TimeUnit.MINUTES;
19 |
20 | import java.lang.annotation.Documented;
21 | import java.lang.annotation.Inherited;
22 | import java.lang.annotation.Retention;
23 | import java.lang.annotation.Target;
24 | import java.util.concurrent.TimeUnit;
25 |
26 | import jakarta.enterprise.context.NormalScope;
27 | import jakarta.enterprise.util.AnnotationLiteral;
28 |
29 | /**
30 | * Specify that a bean is to be pooled.
31 | *
32 | * Pooled beans can have multiple instances that are shared between all threads of an application, but can never be
33 | * called by more than one thread at the same time. When a method call on a pooled bean is performed, an instance is
34 | * selected from the pool and is locked until the method call ends. When performing multiple method calls directly after
35 | * each other, multiple different beans may be used.
36 | *
37 | */
38 | @Inherited
39 | @NormalScope
40 | @Documented
41 | @Target({TYPE, METHOD})
42 | @Retention(RUNTIME)
43 | public @interface Pooled {
44 |
45 | /**
46 | * The maximum number of instances in the pool.
47 | * @return the maximum number of instances
48 | */
49 | int maxNumberOfInstances() default 10;
50 |
51 | /**
52 | * The maximum amount of time to attempt to obtain a lock on an instance from the pool.
53 | * @return the maximum amount of time to try to obtain a lock
54 | */
55 | long instanceLockTimeout() default 5;
56 |
57 | /**
58 | * The {@link TimeUnit} to use for the {@link #instanceLockTimeout()}
59 | * @return the time unit to use
60 | */
61 | TimeUnit instanceLockTimeoutUnit() default MINUTES;
62 |
63 | /**
64 | * The types of {@link Throwable Throwables} which must cause the destruction of a pooled bean instance.
65 | *
66 | * If the pooled bean instances throws any Throwable which is an instance of a type set in the destroyOn property,
67 | * then the bean will be destroyed, except if the throwable is also an instance of a type set in the {@link
68 | * #dontDestroyOn()} property.
69 | *
70 | *
71 | * @return The types of {@link Throwable Throwables} which must cause the destruction of a pooled bean instance
72 | */
73 | Class[] destroyOn() default {};
74 |
75 | /**
76 | * The types of {@link Throwable Throwables} which must not cause the destruction of a pooled bean instance.
77 | *
78 | * If the {@link #destroyOn()} property is empty, but the dontDestroyOn property is not, then the bean will be
79 | * destroyed for any Throwable that isn't an instance of a type in the dontDestroyOn property. When the {@link #destroyOn()} property is not empty, then the dontDestroyOn property takes precedence. If a pooled bean instance throws a throwable which is an instance of a type in both the destroyOn and dontDestroyOn properties, then the bean will not be destroyed.
80 | *
81 | *
82 | * @return The types of {@link Throwable Throwables} which must not cause the destruction of a pooled bean instance.
83 | */
84 | Class[] dontDestroyOn() default {};
85 |
86 | /**
87 | * Supports inline instantiation of the Pooled annotation.
88 | *
89 | * @since 3.0
90 | */
91 | public static final class Literal extends AnnotationLiteral implements Pooled {
92 |
93 | private static final long serialVersionUID = 1L;
94 |
95 | private final int maxNumberOfInstances;
96 | private final long instanceLockTimeout;
97 | private final TimeUnit instanceLockTimeoutUnit;
98 | private final Class[] destroyOn;
99 | private final Class[] dontDestroyOn;
100 |
101 | /**
102 | * Default instance of the {@link Pooled} annotation.
103 | */
104 | public static final Literal INSTANCE = of(
105 | 10,
106 | 5,
107 | MINUTES,
108 | new Class[]{},
109 | new Class[]{}
110 | );
111 |
112 | /**
113 | * Instance of the {@link Pooled} annotation.
114 | *
115 | * @param maxNumberOfInstances The maximum number of instances in the pool.
116 | * @param instanceLockTimeout The maximum amount of time to attempt to obtain a lock on an instance from the pool.
117 | * @param instanceLockTimeoutUnit The {@link TimeUnit} to use for the {@link #instanceLockTimeout()}
118 | * @param destroyOn The types of {@link Throwable Throwables} which must cause the destruction of a pooled bean instance.
119 | * @param dontDestroyOn The types of {@link Throwable Throwables} which must not cause the destruction of a pooled bean instance.
120 |
121 | * @return instance of the {@link Pooled} annotation
122 | */
123 | public static Literal of(
124 | final int maxNumberOfInstances,
125 | final long instanceLockTimeout,
126 | final TimeUnit instanceLockTimeoutUnit,
127 | final Class[] destroyOn,
128 | final Class[] dontDestroyOn
129 |
130 |
131 | ) {
132 | return new Literal(
133 | maxNumberOfInstances,
134 | instanceLockTimeout,
135 | instanceLockTimeoutUnit,
136 | destroyOn,
137 | dontDestroyOn
138 | );
139 | }
140 |
141 | private Literal(
142 | final int maxNumberOfInstances,
143 | final long instanceLockTimeout,
144 | final TimeUnit instanceLockTimeoutUnit,
145 | final Class[] destroyOn,
146 | final Class[] dontDestroyOn
147 | ) {
148 |
149 | this.maxNumberOfInstances = maxNumberOfInstances;
150 | this.instanceLockTimeout = instanceLockTimeout;
151 | this.instanceLockTimeoutUnit = instanceLockTimeoutUnit;
152 | this.destroyOn = destroyOn;
153 | this.dontDestroyOn = dontDestroyOn;
154 | }
155 |
156 |
157 | @Override
158 | public int maxNumberOfInstances() {
159 | return maxNumberOfInstances;
160 | }
161 |
162 | @Override
163 | public long instanceLockTimeout() {
164 | return instanceLockTimeout;
165 | }
166 |
167 | @Override
168 | public TimeUnit instanceLockTimeoutUnit() {
169 | return instanceLockTimeoutUnit;
170 | }
171 |
172 | @Override
173 | public Class[] destroyOn() {
174 | return destroyOn;
175 | }
176 |
177 | @Override
178 | public Class[] dontDestroyOn() {
179 | return dontDestroyOn;
180 | }
181 |
182 | }
183 | }
184 |
185 |
186 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/PooledContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | import java.lang.annotation.Annotation;
16 | import java.util.Map;
17 | import java.util.concurrent.BlockingDeque;
18 | import java.util.concurrent.ConcurrentHashMap;
19 | import java.util.concurrent.LinkedBlockingDeque;
20 | import java.util.stream.IntStream;
21 |
22 | import jakarta.enterprise.context.spi.AlterableContext;
23 | import jakarta.enterprise.context.spi.Contextual;
24 | import jakarta.enterprise.context.spi.CreationalContext;
25 | import jakarta.enterprise.inject.spi.Bean;
26 |
27 | /**
28 | * The {@link AlterableContext} implementation for {@link Pooled} beans.
29 | */
30 | public class PooledContext implements AlterableContext {
31 |
32 | private final ThreadLocal poolScope = ThreadLocal.withInitial(PooledScope::new);
33 | private final Map, InstancePool>> instancePools = new ConcurrentHashMap<>();
34 |
35 | private final Map, Object> dummyInstances = new ConcurrentHashMap<>();
36 |
37 | @Override
38 | public Class extends Annotation> getScope() {
39 | return Pooled.class;
40 | }
41 |
42 | @SuppressWarnings("unchecked")
43 | @Override
44 | public T get(Contextual contextual, CreationalContext creationalContext) {
45 | if (contextual instanceof Bean) {
46 | PoolKey poolKey = poolScope.get().getPoolKey(contextual);
47 |
48 | if (poolKey == null) {
49 | return (T) dummyInstances.computeIfAbsent(contextual, ctx -> contextual.create(creationalContext));
50 | }
51 |
52 | return ((InstancePool) instancePools.get(poolKey.contextual())).getInstance(poolKey, creationalContext);
53 | }
54 |
55 | // TODO add clear error message and pick better exception
56 | throw new IllegalArgumentException();
57 | }
58 |
59 | @SuppressWarnings("unchecked")
60 | @Override
61 | public T get(Contextual contextual) {
62 | if (contextual instanceof Bean) {
63 | PoolKey poolKey = poolScope.get().getPoolKey(contextual);
64 |
65 | if (poolKey == null) {
66 | return (T) dummyInstances.get(contextual);
67 | }
68 |
69 | return ((InstancePool) instancePools.get(poolKey.contextual())).getInstance(poolKey);
70 | }
71 | return null;
72 | }
73 |
74 | @Override
75 | public boolean isActive() {
76 | return true;
77 | }
78 |
79 | @SuppressWarnings("unchecked")
80 | PoolKey allocateBean(Contextual contextual) {
81 | PoolKey poolKey = ((InstancePool) instancePools.get(contextual)).allocateInstance();
82 |
83 | poolScope.get().setPoolKey(poolKey);
84 |
85 | return poolKey;
86 | }
87 |
88 | @SuppressWarnings("unchecked")
89 | void releaseBean(PoolKey poolKey) {
90 | poolScope.get().removePoolKey(poolKey);
91 |
92 | ((InstancePool) instancePools.get(poolKey.contextual())).releaseInstance(poolKey);
93 | }
94 |
95 | boolean hasAllocatedInstanceOf(Bean> bean) {
96 | return poolScope.get().getPoolKey(bean) != null;
97 | }
98 |
99 | public void createInstancePool(Contextual contextual, Pooled poolSettings) {
100 | instancePools.put(contextual, new InstancePool<>(contextual, poolSettings));
101 | }
102 |
103 | boolean mustDestroyBeanWhenCaught(Contextual contextual, Throwable throwable) {
104 | return instancePools.get(contextual).mustDestroyBeanWhenCaught(throwable);
105 | }
106 |
107 | @Override
108 | public void destroy(Contextual> contextual) {
109 | PoolKey> poolKey = poolScope.get().getPoolKey(contextual);
110 |
111 | if (poolKey != null) {
112 | destroyInstance(poolKey);
113 | }
114 | }
115 |
116 | private void destroyInstance(PoolKey poolKey) {
117 | @SuppressWarnings("unchecked")
118 | InstancePool instancePool = (InstancePool) instancePools.get(poolKey.contextual());
119 |
120 | instancePool.destroyInstance(poolKey);
121 | }
122 |
123 | private static class InstancePool {
124 |
125 | private final Contextual contextual;
126 | private final Pooled poolSettings;
127 |
128 | private final Map, Instance> instances = new ConcurrentHashMap<>();
129 | private final BlockingDeque> freeInstanceKeys = new LinkedBlockingDeque<>();
130 |
131 | InstancePool(Contextual contextual, Pooled poolSettings) {
132 | this.contextual = contextual;
133 | this.poolSettings = poolSettings;
134 |
135 | IntStream.range(0, poolSettings.maxNumberOfInstances())
136 | .mapToObj(i -> new PoolKey<>(contextual, i))
137 | .forEach(freeInstanceKeys::add);
138 | }
139 |
140 | T getInstance(PoolKey poolKey) {
141 | if (!poolKey.contextual().equals(contextual)) {
142 | throw new IllegalArgumentException();
143 | }
144 |
145 | Instance instance = instances.get(poolKey);
146 |
147 | if (instance != null) {
148 | return instance.getInstance();
149 | }
150 |
151 | return null;
152 | }
153 |
154 | T getInstance(PoolKey poolKey, CreationalContext context) {
155 | if (!poolKey.contextual().equals(contextual)) {
156 | throw new IllegalArgumentException();
157 | }
158 |
159 | return instances.computeIfAbsent(poolKey, key -> new Instance<>(contextual, context)).getInstance();
160 | }
161 |
162 | PoolKey allocateInstance() {
163 | try {
164 | PoolKey poolKey = freeInstanceKeys.poll(poolSettings.instanceLockTimeout(), poolSettings.instanceLockTimeoutUnit());
165 |
166 | if (poolKey == null) {
167 | // Unable to allocate an instance within the configured timeout
168 | throw new PoolLockTimeoutException();
169 | }
170 |
171 | return poolKey;
172 | } catch (InterruptedException e) {
173 | throw new UncheckedInterruptedException(e);
174 | }
175 | }
176 |
177 | void releaseInstance(PoolKey key) {
178 | if (!contextual.equals(key.contextual())) {
179 | throw new IllegalArgumentException();
180 | }
181 |
182 | freeInstanceKeys.addFirst(key);
183 | }
184 |
185 | void destroyInstance(PoolKey key) {
186 | Instance instance = instances.remove(key);
187 |
188 | instance.destroy(contextual);
189 | }
190 |
191 | boolean mustDestroyBeanWhenCaught(Throwable throwable) {
192 | for (Class throwableType: poolSettings.dontDestroyOn()) {
193 | if (throwableType.isInstance(throwable)) {
194 | return false;
195 | }
196 | }
197 |
198 | if (poolSettings.dontDestroyOn().length > 0 && poolSettings.destroyOn().length == 0) {
199 | return true;
200 | }
201 |
202 | for (Class throwableType: poolSettings.destroyOn()) {
203 | if (throwableType.isInstance(throwable)) {
204 | return true;
205 | }
206 | }
207 |
208 | return false;
209 | }
210 | }
211 |
212 | private static class Instance {
213 |
214 | private final T instance;
215 | private final CreationalContext creationalContext;
216 |
217 | Instance(Contextual contextual, CreationalContext creationalContext) {
218 | this.instance = contextual.create(creationalContext);
219 | this.creationalContext = creationalContext;
220 | }
221 |
222 | T getInstance() {
223 | return instance;
224 | }
225 |
226 | CreationalContext getCreationalContext() {
227 | return creationalContext;
228 | }
229 |
230 | void destroy(Contextual contextual) {
231 | contextual.destroy(instance, creationalContext);
232 | }
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/PooledScope.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | import java.util.HashMap;
16 | import java.util.Map;
17 |
18 | import jakarta.enterprise.context.spi.Contextual;
19 |
20 | class PooledScope {
21 |
22 | private final Map, PoolKey>> allocatedPoolKeys = new HashMap<>();
23 |
24 | public void setPoolKey(PoolKey> pookKey) {
25 | allocatedPoolKeys.put(pookKey.contextual(), pookKey);
26 | }
27 |
28 | public void removePoolKey(PoolKey> poolKey) {
29 | allocatedPoolKeys.remove(poolKey.contextual());
30 | }
31 |
32 | @SuppressWarnings("unchecked")
33 | public PoolKey getPoolKey(Contextual contextual) {
34 | return (PoolKey) allocatedPoolKeys.get(contextual);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/PooledScopeEnabled.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | import static java.lang.annotation.ElementType.METHOD;
16 | import static java.lang.annotation.ElementType.TYPE;
17 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
18 |
19 | import java.lang.annotation.Documented;
20 | import java.lang.annotation.Inherited;
21 | import java.lang.annotation.Retention;
22 | import java.lang.annotation.Target;
23 |
24 | import org.omnifaces.services.asynchronous.Asynchronous;
25 |
26 | import jakarta.enterprise.util.AnnotationLiteral;
27 | import jakarta.interceptor.InterceptorBinding;
28 |
29 | @InterceptorBinding
30 | @Documented
31 | @Inherited
32 | @Retention(RUNTIME)
33 | @Target({TYPE, METHOD})
34 | public @interface PooledScopeEnabled {
35 |
36 | /**
37 | * Supports inline instantiation of the {@link PooledScopeEnabled} annotation.
38 | *
39 | */
40 | public static final class Literal extends AnnotationLiteral implements PooledScopeEnabled {
41 | private static final long serialVersionUID = 1L;
42 |
43 | /**
44 | * Instance of the {@link Asynchronous} annotation.
45 | */
46 | public static final Literal INSTANCE = new Literal();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/PooledScopeInterceptor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | import static jakarta.interceptor.Interceptor.Priority.PLATFORM_BEFORE;
16 |
17 | import java.lang.reflect.InvocationTargetException;
18 |
19 | import jakarta.annotation.Priority;
20 | import jakarta.enterprise.context.spi.CreationalContext;
21 | import jakarta.enterprise.inject.Intercepted;
22 | import jakarta.enterprise.inject.spi.Bean;
23 | import jakarta.enterprise.inject.spi.BeanManager;
24 | import jakarta.inject.Inject;
25 | import jakarta.interceptor.AroundInvoke;
26 | import jakarta.interceptor.Interceptor;
27 | import jakarta.interceptor.InvocationContext;
28 |
29 | @Interceptor
30 | @Priority(PLATFORM_BEFORE)
31 | @PooledScopeEnabled
32 | public class PooledScopeInterceptor {
33 |
34 | @Inject
35 | private BeanManager beanManager;
36 |
37 | @Inject
38 | @Intercepted
39 | private Bean> interceptedBean;
40 |
41 | @AroundInvoke
42 | public Object aroundInvoke(InvocationContext ctx) throws Throwable {
43 | PooledContext context = (PooledContext) beanManager.getContext(Pooled.class);
44 |
45 | if (context.hasAllocatedInstanceOf(interceptedBean)) {
46 | return ctx.proceed();
47 | }
48 |
49 | PoolKey> poolKey = context.allocateBean(interceptedBean);
50 |
51 | try {
52 | CreationalContext> creationalContext = beanManager.createCreationalContext(interceptedBean);
53 | Object reference = beanManager.getReference(interceptedBean, interceptedBean.getBeanClass(), creationalContext);
54 |
55 | return proceedOnInstance(ctx, reference);
56 | }
57 | catch (Throwable t) {
58 | destroyBeanIfNeeded(context, t);
59 |
60 | throw t;
61 | }
62 | finally {
63 | context.releaseBean(poolKey);
64 | }
65 | }
66 |
67 | private Object proceedOnInstance(InvocationContext ctx, Object instance) throws Throwable {
68 | try {
69 | return ctx.getMethod().invoke(instance, ctx.getParameters());
70 | }
71 | catch(InvocationTargetException e) {
72 | throw e.getCause();
73 | }
74 | }
75 |
76 | private void destroyBeanIfNeeded(PooledContext pooledContext, Throwable throwable) throws Throwable {
77 | if (pooledContext.mustDestroyBeanWhenCaught(interceptedBean, throwable)) {
78 | pooledContext.destroy(interceptedBean);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/pooled/UncheckedInterruptedException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.pooled;
14 |
15 | // TODO move to OmniUtils??
16 | public class UncheckedInterruptedException extends RuntimeException {
17 |
18 | public UncheckedInterruptedException(String message, Throwable cause) {
19 | super(message, cause);
20 | }
21 |
22 | public UncheckedInterruptedException(Throwable cause) {
23 | super(cause);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/util/AnnotatedMethodWrapper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.util;
14 |
15 | import java.lang.annotation.Annotation;
16 | import java.lang.reflect.Method;
17 | import java.lang.reflect.Type;
18 | import java.util.Collections;
19 | import java.util.HashSet;
20 | import java.util.List;
21 | import java.util.Set;
22 |
23 | import jakarta.enterprise.inject.spi.AnnotatedMethod;
24 | import jakarta.enterprise.inject.spi.AnnotatedParameter;
25 | import jakarta.enterprise.inject.spi.AnnotatedType;
26 |
27 | public class AnnotatedMethodWrapper implements AnnotatedMethod {
28 |
29 | private AnnotatedMethod wrappedAnnotatedMethod;
30 |
31 | private Set annotations;
32 |
33 | public AnnotatedMethodWrapper(AnnotatedMethod wrappedAnnotatedMethod) {
34 | this.wrappedAnnotatedMethod = wrappedAnnotatedMethod;
35 |
36 | annotations = new HashSet<>(wrappedAnnotatedMethod.getAnnotations());
37 | }
38 |
39 | @Override
40 | public List> getParameters() {
41 | return wrappedAnnotatedMethod.getParameters();
42 | }
43 |
44 | @Override
45 | public AnnotatedType getDeclaringType() {
46 | return wrappedAnnotatedMethod.getDeclaringType();
47 | }
48 |
49 | @Override
50 | public boolean isStatic() {
51 | return wrappedAnnotatedMethod.isStatic();
52 | }
53 |
54 | @Override
55 | public T getAnnotation(Class annotationType) {
56 | for (Annotation annotation : annotations) {
57 | if (annotationType.isInstance(annotation)) {
58 | return annotationType.cast(annotation);
59 | }
60 | }
61 |
62 | return null;
63 | }
64 |
65 | @Override
66 | public Set getAnnotations() {
67 | return Collections.unmodifiableSet(annotations);
68 | }
69 |
70 | @Override
71 | public Type getBaseType() {
72 | return wrappedAnnotatedMethod.getBaseType();
73 | }
74 |
75 | @Override
76 | public Set getTypeClosure() {
77 | return wrappedAnnotatedMethod.getTypeClosure();
78 | }
79 |
80 | @Override
81 | public boolean isAnnotationPresent(Class extends Annotation> annotationType) {
82 | for (Annotation annotation : annotations) {
83 | if (annotationType.isInstance(annotation)) {
84 | return true;
85 | }
86 | }
87 |
88 | return false;
89 | }
90 |
91 | @Override
92 | public Method getJavaMember() {
93 | return wrappedAnnotatedMethod.getJavaMember();
94 | }
95 |
96 | public void addAnnotation(Annotation annotation) {
97 | annotations.add(annotation);
98 | }
99 |
100 | public void removeAnnotation(Annotation annotation) {
101 | annotations.remove(annotation);
102 | }
103 |
104 | public void removeAnnotation(Class extends Annotation> annotationType) {
105 | Annotation annotation = getAnnotation(annotationType);
106 | if (annotation != null ) {
107 | removeAnnotation(annotation);
108 | }
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/util/AnnotatedTypeWrapper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.util;
14 |
15 | import java.lang.annotation.Annotation;
16 | import java.lang.reflect.Type;
17 | import java.util.HashSet;
18 | import java.util.Set;
19 |
20 | import jakarta.enterprise.inject.spi.AnnotatedConstructor;
21 | import jakarta.enterprise.inject.spi.AnnotatedField;
22 | import jakarta.enterprise.inject.spi.AnnotatedMethod;
23 | import jakarta.enterprise.inject.spi.AnnotatedType;
24 |
25 | public class AnnotatedTypeWrapper implements AnnotatedType {
26 |
27 | private AnnotatedType wrappedAnnotatedType;
28 |
29 | private Set annotations = new HashSet<>();
30 | private Set> annotatedMethods = new HashSet<>();
31 | private Set> annotatedFields = new HashSet<>();
32 |
33 | public AnnotatedTypeWrapper(AnnotatedType wrappedAnnotatedType) {
34 | this.wrappedAnnotatedType = wrappedAnnotatedType;
35 |
36 | annotations.addAll(wrappedAnnotatedType.getAnnotations());
37 | annotatedMethods.addAll(wrappedAnnotatedType.getMethods());
38 | annotatedFields.addAll(wrappedAnnotatedType.getFields());
39 | }
40 |
41 | @Override
42 | public A getAnnotation(Class annotationType) {
43 | return wrappedAnnotatedType.getAnnotation(annotationType);
44 | }
45 |
46 | @Override
47 | public Set getAnnotations() {
48 | return annotations;
49 | }
50 |
51 | @Override
52 | public Type getBaseType() {
53 | return wrappedAnnotatedType.getBaseType();
54 | }
55 |
56 | @Override
57 | public Set> getConstructors() {
58 | return wrappedAnnotatedType.getConstructors();
59 | }
60 |
61 | @Override
62 | public Set> getFields() {
63 | return annotatedFields;
64 | }
65 |
66 | @Override
67 | public Class getJavaClass() {
68 | return wrappedAnnotatedType.getJavaClass();
69 | }
70 |
71 | @Override
72 | public Set> getMethods() {
73 | return annotatedMethods;
74 | }
75 |
76 | @Override
77 | public Set getTypeClosure() {
78 | return wrappedAnnotatedType.getTypeClosure();
79 | }
80 |
81 | @Override
82 | public boolean isAnnotationPresent(Class extends Annotation> annotationType) {
83 | for (Annotation annotation : annotations) {
84 | if (annotationType.isInstance(annotation)) {
85 | return true;
86 | }
87 | }
88 |
89 | return false;
90 | }
91 |
92 | public void addAnnotation(Annotation annotation) {
93 | annotations.add(annotation);
94 | }
95 |
96 | public void removeAnnotation(Annotation annotation) {
97 | annotations.remove(annotation);
98 | }
99 |
100 | public void removeAnnotation(Class extends Annotation> annotationType) {
101 | Annotation annotation = getAnnotation(annotationType);
102 | if (annotation != null ) {
103 | removeAnnotation(annotation);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/org/omnifaces/services/util/CdiUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.services.util;
14 |
15 | import java.lang.annotation.Annotation;
16 | import java.util.Arrays;
17 | import java.util.LinkedList;
18 | import java.util.Optional;
19 | import java.util.Queue;
20 | import java.util.Set;
21 |
22 | import jakarta.enterprise.inject.spi.Annotated;
23 | import jakarta.enterprise.inject.spi.Bean;
24 | import jakarta.enterprise.inject.spi.BeanManager;
25 | import jakarta.interceptor.InvocationContext;
26 |
27 | public class CdiUtils {
28 |
29 | public static T getInterceptorBindingAnnotation(InvocationContext invocationContext, BeanManager beanManager, Bean> interceptedBean, Class type) {
30 | Optional optionalAnnotation = getAnnotation(beanManager, interceptedBean.getBeanClass(), (Class) type);
31 | if (optionalAnnotation.isPresent()) {
32 | return optionalAnnotation.get();
33 | }
34 |
35 | @SuppressWarnings("unchecked")
36 | Set bindings = (Set) invocationContext.getContextData().get("org.jboss.weld.interceptor.bindings");
37 | if (bindings != null) {
38 | optionalAnnotation = bindings.stream()
39 | .filter(annotation -> annotation.annotationType().equals(type))
40 | .findAny()
41 | .map(annotation -> type.cast(annotation));
42 |
43 | if (optionalAnnotation.isPresent()) {
44 | return optionalAnnotation.get();
45 | }
46 | }
47 |
48 | throw new IllegalStateException("@" + type + " not present on " + interceptedBean.getBeanClass());
49 | }
50 |
51 | public static Optional getAnnotation(BeanManager beanManager, Class> annotatedClass, Class annotationType) {
52 |
53 | if (annotatedClass.isAnnotationPresent(annotationType)) {
54 | return Optional.of(annotatedClass.getAnnotation(annotationType));
55 | }
56 |
57 | Queue annotations = new LinkedList<>(Arrays.asList(annotatedClass.getAnnotations()));
58 |
59 | while (!annotations.isEmpty()) {
60 | Annotation annotation = annotations.remove();
61 |
62 | if (annotation.annotationType().equals(annotationType)) {
63 | return Optional.of(annotationType.cast(annotation));
64 | }
65 |
66 | if (beanManager.isStereotype(annotation.annotationType())) {
67 | annotations.addAll(
68 | beanManager.getStereotypeDefinition(
69 | annotation.annotationType()
70 | )
71 | );
72 | }
73 | }
74 |
75 | return Optional.empty();
76 | }
77 |
78 | public static Optional getAnnotation(BeanManager beanManager, Annotated annotated, Class annotationType) {
79 |
80 | annotated.getAnnotation(annotationType);
81 |
82 | if (annotated.getAnnotations().isEmpty()) {
83 | return Optional.empty();
84 | }
85 |
86 | if (annotated.isAnnotationPresent(annotationType)) {
87 | return Optional.of(annotated.getAnnotation(annotationType));
88 | }
89 |
90 | Queue annotations = new LinkedList<>(annotated.getAnnotations());
91 |
92 | while (!annotations.isEmpty()) {
93 | Annotation annotation = annotations.remove();
94 |
95 | if (annotation.annotationType().equals(annotationType)) {
96 | return Optional.of(annotationType.cast(annotation));
97 | }
98 |
99 | if (beanManager.isStereotype(annotation.annotationType())) {
100 | annotations.addAll(
101 | beanManager.getStereotypeDefinition(
102 | annotation.annotationType()
103 | )
104 | );
105 | }
106 | }
107 |
108 | return Optional.empty();
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension:
--------------------------------------------------------------------------------
1 | org.omnifaces.services.CdiExtension
--------------------------------------------------------------------------------
/src/test/java/org/omnifaces/test/services/pooled/PooledTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.test.services.pooled;
14 |
15 | import static org.junit.jupiter.api.Assertions.assertEquals;
16 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
17 | import static org.junit.jupiter.api.Assertions.assertThrows;
18 |
19 | import org.jboss.arquillian.junit5.ArquillianExtension;
20 | import org.junit.jupiter.api.Disabled;
21 | import org.junit.jupiter.api.DisplayName;
22 | import org.junit.jupiter.api.Test;
23 | import org.junit.jupiter.api.extension.ExtendWith;
24 |
25 | import jakarta.inject.Inject;
26 |
27 | @ExtendWith(ArquillianExtension.class)
28 | @DisplayName("@Pooled")
29 | @Disabled("Complains about DependencyInjectionArquillianExtension and unnamed module")
30 | public class PooledTest {
31 |
32 | // @Deployment
33 | // public static Archive> createDeployment() {
34 | // return create(WebArchive.class)
35 | // .addAsManifestResource(INSTANCE, "beans.xml")
36 | // .addClasses(SingleInstancePooledBean.class)
37 | // .addAsLibraries(create(JavaArchive.class)
38 | // .addAsManifestResource(INSTANCE, "beans.xml")
39 | // .addAsServiceProvider(Extension.class, CdiExtension.class)
40 | // .addPackages(true, "org.omnifaces.services.pooled")
41 | // .addPackages(true, "org.omnifaces.services.util")
42 | // )
43 | // .addAsLibraries(Maven.resolver()
44 | // .loadPomFromFile("pom.xml")
45 | // .resolve("org.omnifaces:omniutils")
46 | // .withoutTransitivity()
47 | // .asSingleFile());
48 | // }
49 |
50 | @Inject
51 | private SingleInstancePooledBean singleInstancePooledBean;
52 |
53 | @Test
54 | @DisplayName("with a single instance configured will always returns the same instance.")
55 | public void bean_withASingleInstanceConfigured_willAlwaysCauseSameInstanceToBeUsed() {
56 | int identityHashCode = singleInstancePooledBean.getIdentityHashCode();
57 |
58 | assertEquals(identityHashCode, singleInstancePooledBean.getIdentityHashCode());
59 | }
60 |
61 | @Test
62 | @DisplayName(
63 | "with an invocation that throws an exception not in the destroyOn field does not destroy the instance.")
64 | public void bean_withInvocationThrowingANonDestroyingException_doesNotDestroyInstance() {
65 | int identityHashCode = singleInstancePooledBean.getIdentityHashCode();
66 |
67 | assertThrows(Exception.class, () -> singleInstancePooledBean.throwException(Exception::new));
68 |
69 | assertEquals(identityHashCode, singleInstancePooledBean.getIdentityHashCode());
70 | }
71 |
72 | @Test
73 | @DisplayName(
74 | "with an invocation that throws an exception explicitly listed in the dontDestroyOn field does not destroy the instance.")
75 | public void bean_withInvocationThrowingAnExplicitNonDestroyingException_doesNotDestroyInstance() {
76 | int identityHashCode = singleInstancePooledBean.getIdentityHashCode();
77 |
78 | assertThrows(IllegalArgumentException.class,
79 | () -> singleInstancePooledBean.throwException(IllegalArgumentException::new));
80 |
81 | assertEquals(identityHashCode, singleInstancePooledBean.getIdentityHashCode());
82 | }
83 |
84 |
85 | @Test
86 | @DisplayName(
87 | "with an invocation that throws an exception explicitly listed in the destroyOn field destroys the instance.")
88 | public void bean_withInvocationThrowingAnExplicitDestroyingException_destroysInstance() {
89 | int identityHashCode = singleInstancePooledBean.getIdentityHashCode();
90 |
91 | assertThrows(RuntimeException.class, () -> singleInstancePooledBean.throwException(RuntimeException::new));
92 |
93 | assertNotEquals(identityHashCode, singleInstancePooledBean.getIdentityHashCode());
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/test/java/org/omnifaces/test/services/pooled/SingleInstancePooledBean.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.test.services.pooled;
14 |
15 | import java.util.function.Supplier;
16 |
17 | import org.omnifaces.services.pooled.Pooled;
18 |
19 | @Pooled(destroyOn = RuntimeException.class, dontDestroyOn = IllegalArgumentException.class, maxNumberOfInstances = 1)
20 | public class SingleInstancePooledBean {
21 |
22 | public int getIdentityHashCode() {
23 | return System.identityHashCode(this);
24 | }
25 |
26 | public void throwException(Supplier exceptionSupplier) throws E {
27 | throw exceptionSupplier.get();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/org/omnifaces/test/services/pooled/testing/DependencyInjectionArquillianExtension.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 OmniFaces
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License. You may obtain a copy of the License at
6 | *
7 | * https://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | */
13 | package org.omnifaces.test.services.pooled.testing;
14 |
15 | import java.util.Optional;
16 | import java.util.function.Predicate;
17 |
18 | import jakarta.enterprise.inject.spi.BeanManager;
19 | import jakarta.enterprise.inject.spi.CDI;
20 | import jakarta.enterprise.inject.spi.InjectionTarget;
21 |
22 | import org.junit.jupiter.api.extension.ExtensionContext;
23 | import org.junit.jupiter.api.extension.TestInstancePostProcessor;
24 |
25 | /**
26 | * JUnit extension to enable CDI based dependency injection in test instances when running in an Arquillian container.
27 | */
28 | public class DependencyInjectionArquillianExtension implements TestInstancePostProcessor {
29 |
30 | private static final Predicate isInsideArquillian =
31 | (context) -> context.getConfigurationParameter("insideArquillian").map(Boolean::parseBoolean).orElse(false);
32 |
33 | @Override
34 | public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) {
35 | getCdi(extensionContext).ifPresent(cdi -> {
36 | var beanManager = cdi.getBeanManager();
37 | var testClassType = beanManager.createAnnotatedType(testInstance.getClass());
38 | var injectionTargetFactory = beanManager.getInjectionTargetFactory(testClassType);
39 | var injectionTarget = injectionTargetFactory.createInjectionTarget(null);
40 |
41 | inject(injectionTarget, beanManager, testInstance);
42 | });
43 | }
44 |
45 | @SuppressWarnings("unchecked")
46 | private static void inject(InjectionTarget injectionTarget, BeanManager beanManager, Object instance) {
47 | injectionTarget.inject((T) instance, beanManager.createCreationalContext(null));
48 | }
49 |
50 | private static Optional> getCdi(ExtensionContext context) {
51 | if (context.getConfigurationParameter("insideArquillian").map(Boolean::parseBoolean).orElse(false)) {
52 | return Optional.of(CDI.current());
53 | }
54 |
55 | return Optional.empty();
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/test/resources/arquillian.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
18 |
19 |
20 |
21 |
22 |
23 | xml
24 | ${arquillian.liberty.wlpHome}
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/test/resources/server.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 | jakartaee-9.1
21 | localConnector-1.0
22 |
23 |
24 |
25 |
26 |
29 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------