Skip to content

Commit a02b6d4

Browse files
committed
Add Uni interceptor to manage @CallScope
1 parent 616d47b commit a02b6d4

10 files changed

Lines changed: 395 additions & 12 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
workflow_dispatch:
44
push:
55
jobs:
6-
GuicedInjection:
6+
GuicedClient:
77
uses: GuicedEE/Workflows/.github/workflows/projects.yml@master
88
with:
99
baseDir: ''

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ Thumbs.db
2020
flatter.pom**/*.classpath
2121
/.idea/
2222
/dependency-reduced-pom.xml
23+
/vscode/

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
# GuicedEE Inject Client
1+
# 🚀 GuicedEE Inject Client
22

3-
An extension library for GuicedEE that provides client‑side injection utilities, lifecycle hooks, and module wiring to simplify bootstrapping client components in Java applications.
3+
[![JDK](https://img.shields.io/badge/JDK-25%2B-0A7?logo=java)](https://openjdk.org/projects/jdk/25/)
4+
[![Build](https://img.shields.io/badge/Build-Maven-C71A36?logo=apachemaven)](https://maven.apache.org/)
5+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
6+
7+
<!-- Tech icons row -->
8+
![Guice](https://img.shields.io/badge/Guice-Core-2F4F4F)
9+
![GuicedEE](https://img.shields.io/badge/GuicedEE-Client-0A7)
10+
![JPMS](https://img.shields.io/badge/JPMS-Modules-0A7)
411

512
This repository follows a documentation‑first, stage‑gated workflow using the Rules Repository model (Pact → Rules → Guides → Implementation). See the key docs below for full details.
613

src/main/java/com/guicedee/client/implementations/GuicedEEClientStartup.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.guicedee.client.IGuiceContext;
44
import com.guicedee.client.services.lifecycle.IGuicePreStartup;
55
import io.vertx.core.Future;
6+
import io.smallrye.mutiny.infrastructure.Infrastructure;
67
import lombok.extern.log4j.Log4j2;
78

89
import java.util.List;
@@ -28,6 +29,8 @@ public List<Future<Boolean>> onStartup()
2829
.setAnnotationScanning(true)
2930
;
3031
log.debug("✅ GuicedEE scanning options configured successfully");
32+
log.trace("🔄 Reloading Mutiny Uni interceptors");
33+
Infrastructure.reloadUniInterceptors();
3134
log.trace("✅ GuicedEE Client initialized successfully");
3235
}
3336
catch (Throwable T)

src/main/java/com/guicedee/client/scopes/CallScopeProperties.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import java.io.Serial;
88
import java.io.Serializable;
9+
import java.util.ArrayList;
910
import java.util.HashMap;
11+
import java.util.List;
1012
import java.util.Map;
1113

1214
@CallScope
@@ -20,10 +22,15 @@ public class CallScopeProperties implements Serializable
2022
/**
2123
* The source of the call scope entry
2224
*/
23-
private CallScopeSource source;
25+
private CallScopeSource source = CallScopeSource.Unknown;
2426
/**
2527
* Any properties to carry within the call scope
2628
*/
2729
private Map<Object, Object> properties = new HashMap<>();
2830

31+
/**
32+
* Ordered list of locations where this scope was touched (creation, bounces, Uni starts).
33+
*/
34+
private List<String> touches = new ArrayList<>();
35+
2936
}

src/main/java/com/guicedee/client/scopes/CallScopeSource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public enum CallScopeSource
44
{
5+
Unknown,
56
Http,
67
WebSocket,
78
RabbitMQ,
@@ -10,6 +11,7 @@ public enum CallScopeSource
1011
Transaction,
1112
Test,
1213
Rest,
14+
Persistence,
1315
WebService,
1416
Startup,
1517
VertXConsumer,

src/main/java/com/guicedee/client/scopes/CallScoper.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ public void enter()
4141
{
4242
checkState(values.get() == null, "A scoping block is already in progress");
4343
values.set(Maps.<Key<?>, Object>newHashMap());
44-
seed(CallScopeProperties.class, new CallScopeProperties());
44+
// Seed CallScopeProperties and explicitly mark the source as Unknown on scope start
45+
CallScopeProperties props = new CallScopeProperties();
46+
props.setSource(CallScopeSource.Unknown);
47+
seed(CallScopeProperties.class, props);
4548
@SuppressWarnings("rawtypes")
4649
Set<IOnCallScopeEnter> scopeEnters = IGuiceContext.loaderToSet(ServiceLoader.load(IOnCallScopeEnter.class));
4750
for (IOnCallScopeEnter<?> scopeEnter : scopeEnters)
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package com.guicedee.client.scopes.mutiny;
2+
3+
import com.google.inject.Key;
4+
import com.guicedee.client.IGuiceContext;
5+
import com.guicedee.client.scopes.CallScopeProperties;
6+
import com.guicedee.client.scopes.CallScoper;
7+
import io.smallrye.mutiny.Uni;
8+
import io.smallrye.mutiny.infrastructure.UniInterceptor;
9+
import io.smallrye.mutiny.operators.AbstractUni;
10+
import io.smallrye.mutiny.subscription.UniSubscriber;
11+
import io.smallrye.mutiny.subscription.UniSubscription;
12+
13+
import java.util.Collections;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
import java.util.concurrent.atomic.AtomicBoolean;
17+
18+
public class CallScopeUniInterceptor
19+
implements UniInterceptor
20+
{
21+
private static final Key<CallScopeProperties> CALL_SCOPE_PROPERTIES_KEY = Key.get(CallScopeProperties.class);
22+
23+
@Override
24+
public <T> Uni<T> onUniCreation(Uni<T> uni)
25+
{
26+
// Avoid re-wrapping if this interceptor already produced the Uni instance
27+
if (uni instanceof CallScopeAwareUni)
28+
{
29+
return uni;
30+
}
31+
CallScoper callScoper = IGuiceContext.get(CallScoper.class);
32+
if (callScoper.isStartedScope())
33+
{
34+
recordTouch(callScoper, "uni-creation", captureLocation());
35+
}
36+
Map<Key<?>, Object> snapshot = captureCallScope(callScoper);
37+
return new CallScopeAwareUni<>(uni, snapshot);
38+
}
39+
40+
private Map<Key<?>, Object> captureCallScope(CallScoper callScoper)
41+
{
42+
if (!callScoper.isStartedScope())
43+
{
44+
return Collections.emptyMap();
45+
}
46+
Map<Key<?>, Object> values = callScoper.getValues();
47+
if (values == null || values.isEmpty())
48+
{
49+
return Collections.emptyMap();
50+
}
51+
return new HashMap<>(values);
52+
}
53+
54+
private static final class CallScopeAwareUni<T> extends AbstractUni<T>
55+
{
56+
private final Uni<T> upstream;
57+
private final Map<Key<?>, Object> capturedValues;
58+
59+
private CallScopeAwareUni(Uni<T> upstream, Map<Key<?>, Object> capturedValues)
60+
{
61+
this.upstream = upstream;
62+
this.capturedValues = capturedValues.isEmpty() ? Collections.emptyMap() : capturedValues;
63+
}
64+
65+
@Override
66+
public void subscribe(UniSubscriber<? super T> subscriber)
67+
{
68+
CallScoper callScoper = IGuiceContext.get(CallScoper.class);
69+
boolean startedHere = false;
70+
71+
if (!callScoper.isStartedScope())
72+
{
73+
callScoper.enter();
74+
startedHere = true;
75+
if (!capturedValues.isEmpty())
76+
{
77+
callScoper.setValues(capturedValues);
78+
}
79+
recordTouch(callScoper, "uni-bounce", captureLocation());
80+
}
81+
82+
ScopedUniSubscriber<T> scopedSubscriber = new ScopedUniSubscriber<>(subscriber, callScoper, startedHere);
83+
try
84+
{
85+
recordTouch(callScoper, startedHere ? "uni-bounce" : "uni-subscribe", captureLocation());
86+
AbstractUni.subscribe(upstream, scopedSubscriber);
87+
}
88+
catch (Throwable t)
89+
{
90+
if (startedHere)
91+
{
92+
safeExit(callScoper);
93+
}
94+
throw t;
95+
}
96+
}
97+
}
98+
99+
private static final class ScopedUniSubscriber<T> implements UniSubscriber<T>
100+
{
101+
private final UniSubscriber<? super T> delegate;
102+
private final CallScoper callScoper;
103+
private final boolean startedScope;
104+
private final AtomicBoolean ended = new AtomicBoolean(false);
105+
106+
private ScopedUniSubscriber(UniSubscriber<? super T> delegate, CallScoper callScoper, boolean startedScope)
107+
{
108+
this.delegate = delegate;
109+
this.callScoper = callScoper;
110+
this.startedScope = startedScope;
111+
}
112+
113+
@Override
114+
public void onSubscribe(UniSubscription subscription)
115+
{
116+
UniSubscription wrapped = startedScope ? new ScopedUniSubscription(subscription, this::endScope) : subscription;
117+
try
118+
{
119+
delegate.onSubscribe(wrapped);
120+
}
121+
catch (Throwable t)
122+
{
123+
endScope();
124+
throw t;
125+
}
126+
}
127+
128+
@Override
129+
public void onItem(T item)
130+
{
131+
try
132+
{
133+
delegate.onItem(item);
134+
}
135+
finally
136+
{
137+
endScope();
138+
}
139+
}
140+
141+
@Override
142+
public void onFailure(Throwable failure)
143+
{
144+
try
145+
{
146+
delegate.onFailure(failure);
147+
}
148+
finally
149+
{
150+
endScope();
151+
}
152+
}
153+
154+
private void endScope()
155+
{
156+
if (startedScope && ended.compareAndSet(false, true))
157+
{
158+
safeExit(callScoper);
159+
}
160+
}
161+
}
162+
163+
private static final class ScopedUniSubscription implements UniSubscription
164+
{
165+
private final UniSubscription delegate;
166+
private final Runnable onCancel;
167+
168+
private ScopedUniSubscription(UniSubscription delegate, Runnable onCancel)
169+
{
170+
this.delegate = delegate;
171+
this.onCancel = onCancel;
172+
}
173+
174+
@Override
175+
public void request(long n)
176+
{
177+
delegate.request(n);
178+
}
179+
180+
@Override
181+
public void cancel()
182+
{
183+
try
184+
{
185+
delegate.cancel();
186+
}
187+
finally
188+
{
189+
onCancel.run();
190+
}
191+
}
192+
}
193+
194+
private static void safeExit(CallScoper callScoper)
195+
{
196+
try
197+
{
198+
callScoper.exit();
199+
}
200+
catch (IllegalStateException ignored)
201+
{
202+
// Scope already exited
203+
}
204+
}
205+
206+
private static void recordTouch(CallScoper callScoper, String reason, String location)
207+
{
208+
CallScopeProperties properties = getCallScopeProperties(callScoper);
209+
if (properties != null)
210+
{
211+
properties.getTouches().add(reason + "@" + location);
212+
}
213+
}
214+
215+
private static CallScopeProperties getCallScopeProperties(CallScoper callScoper)
216+
{
217+
Map<Key<?>, Object> values = callScoper.getValues();
218+
if (values == null)
219+
{
220+
return null;
221+
}
222+
Object properties = values.get(CALL_SCOPE_PROPERTIES_KEY);
223+
if (properties instanceof CallScopeProperties)
224+
{
225+
return (CallScopeProperties) properties;
226+
}
227+
return null;
228+
}
229+
230+
private static String captureLocation()
231+
{
232+
return StackWalker
233+
.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
234+
.walk(stream -> stream
235+
.filter(f -> !f.getClassName().startsWith("io.smallrye.mutiny"))
236+
.filter(f -> !f.getClassName().equals(CallScopeUniInterceptor.class.getName()))
237+
.findFirst()
238+
.map(f -> f.getClassName() + "#" + f.getMethodName() + ":" + f.getLineNumber())
239+
.orElse("unknown"));
240+
}
241+
}

0 commit comments

Comments
 (0)