Skip to content

Commit 1482b89

Browse files
authored
Interceptor Redo (helidon-io#6658)
* Resolves issue helidon-io#6629
1 parent 2869b8b commit 1482b89

19 files changed

Lines changed: 599 additions & 39 deletions

File tree

pico/api/src/main/java/io/helidon/pico/api/Interceptor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public interface Interceptor {
3232
* @param args the arguments to the call
3333
* @param <V> the return value type (or {@link Void} for void method elements)
3434
* @return the return value to the caller
35+
* @throws InvocationException if there are errors during invocation chain processing
3536
*/
3637
<V> V proceed(InvocationContext ctx, Chain<V> chain, Object... args);
3738

@@ -43,13 +44,14 @@ public interface Interceptor {
4344
*/
4445
interface Chain<V> {
4546
/**
46-
* Call the next interceptor in line, or finishing with the call to the service provider.
47+
* Call the next interceptor in line, or finishing with the call to the service provider being intercepted.
4748
* Note that that arguments are passed by reference to each interceptor ultimately leading up to the final
48-
* call to the underlying intercepted target. Callers can mutable the arguments passed directly on the provided array
49+
* call to the underlying intercepted target. Callers can mutate the arguments passed directly on the provided array
4950
* instance.
5051
*
5152
* @param args the arguments passed
5253
* @return the result of the call
54+
* @throws InvocationException if there are errors during invocation chain processing
5355
*/
5456
V proceed(Object[] args);
5557
}

pico/api/src/main/java/io/helidon/pico/api/InvocationException.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,59 @@
2121
*/
2222
public class InvocationException extends PicoServiceProviderException {
2323

24+
/**
25+
* Tracks whether the target being intercepted was called once successfully - meaning that the target was called and it
26+
* did not result in any exception being thrown.
27+
*/
28+
private final boolean targetWasCalled;
29+
30+
/**
31+
* Constructor.
32+
*
33+
* @param msg the message
34+
* @param targetWasCalled set to true if the target of interception was ultimately called successfully
35+
*/
36+
public InvocationException(String msg,
37+
boolean targetWasCalled) {
38+
super(msg);
39+
this.targetWasCalled = targetWasCalled;
40+
}
41+
/**
42+
* Constructor.
43+
*
44+
* @param msg the message
45+
* @param cause the root cause
46+
* @param targetWasCalled set to true if the target of interception was ultimately called successfully
47+
*/
48+
public InvocationException(String msg,
49+
Throwable cause,
50+
boolean targetWasCalled) {
51+
super(msg, cause);
52+
this.targetWasCalled = targetWasCalled;
53+
}
2454
/**
2555
* Constructor.
2656
*
2757
* @param msg the message
2858
* @param cause the root cause
2959
* @param serviceProvider the service provider
60+
* @param targetWasCalled set to true if the target of interception was ultimately called successfully
3061
*/
31-
public InvocationException(String msg, Throwable cause, ServiceProvider<?> serviceProvider) {
62+
public InvocationException(String msg,
63+
Throwable cause,
64+
ServiceProvider<?> serviceProvider,
65+
boolean targetWasCalled) {
3266
super(msg, cause, serviceProvider);
67+
this.targetWasCalled = targetWasCalled;
68+
}
69+
70+
/**
71+
* Returns true if the final target of interception was ultimately called.
72+
*
73+
* @return if the target being intercepted was ultimately called successfully
74+
*/
75+
public boolean targetWasCalled() {
76+
return targetWasCalled;
3377
}
3478

3579
}

pico/runtime/src/main/java/io/helidon/pico/runtime/AbstractServiceProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public void interceptor(ServiceProvider<?> interceptor) {
264264
}
265265
this.interceptor = interceptor;
266266
if (interceptor instanceof AbstractServiceProvider<?>) {
267-
((AbstractServiceProvider) interceptor).intercepted(this);
267+
((AbstractServiceProvider<?>) interceptor).intercepted(this);
268268
}
269269
}
270270

pico/runtime/src/main/java/io/helidon/pico/runtime/InterceptedMethod.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,13 @@ public InvocationContext ctx() {
128128
public V apply(Object... args) {
129129
try {
130130
return invoke(args);
131-
} catch (RuntimeException e) {
132-
throw e;
133131
} catch (Throwable t) {
134-
throw new InvocationException(t.getMessage(), t, ctx.serviceProvider());
132+
boolean targetWasCalledSuccessfully = false;
133+
if (t instanceof InvocationException) {
134+
targetWasCalledSuccessfully = ((InvocationException) t).targetWasCalled();
135+
}
136+
137+
throw new InvocationException(t.getMessage(), t, ctx.serviceProvider(), targetWasCalledSuccessfully);
135138
}
136139
}
137140

pico/runtime/src/main/java/io/helidon/pico/runtime/Invocation.java

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,35 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22-
import java.util.ListIterator;
2322
import java.util.Objects;
2423
import java.util.function.Function;
2524

2625
import io.helidon.pico.api.Interceptor;
2726
import io.helidon.pico.api.InvocationContext;
27+
import io.helidon.pico.api.InvocationException;
2828
import io.helidon.pico.api.ServiceProvider;
2929

3030
import jakarta.inject.Provider;
3131

3232
/**
3333
* Handles the invocation of {@link Interceptor} methods.
34+
* Note that upon a successful call to the {@link io.helidon.pico.api.Interceptor.Chain#proceed(Object[])} or to the ultimate
35+
* target, the invocation will be prevented from being executed again.
3436
*
3537
* @see io.helidon.pico.api.InvocationContext
3638
* @param <V> the invocation type
3739
*/
3840
public class Invocation<V> implements Interceptor.Chain<V> {
3941
private final InvocationContext ctx;
40-
private final ListIterator<Provider<Interceptor>> interceptorIterator;
42+
private final List<Provider<Interceptor>> interceptors;
43+
private int interceptorPos;
4144
private Function<Object[], V> call;
4245

4346
private Invocation(InvocationContext ctx,
4447
Function<Object[], V> call) {
4548
this.ctx = ctx;
4649
this.call = Objects.requireNonNull(call);
47-
this.interceptorIterator = ctx.interceptors().listIterator();
50+
this.interceptors = List.copyOf(ctx.interceptors());
4851
}
4952

5053
@Override
@@ -60,25 +63,43 @@ public String toString() {
6063
* @param args the call arguments
6164
* @param <V> the type returned from the method element
6265
* @return the invocation instance
66+
* @throws InvocationException if there are errors during invocation chain processing
6367
*/
6468
@SuppressWarnings({"unchecked", "rawtypes"})
6569
public static <V> V createInvokeAndSupply(InvocationContext ctx,
6670
Function<Object[], V> call,
6771
Object[] args) {
6872
if (ctx.interceptors().isEmpty()) {
69-
return call.apply(args);
73+
try {
74+
return call.apply(args);
75+
} catch (Throwable t) {
76+
throw new InvocationException("Error in interceptor chain processing", t, true);
77+
}
7078
} else {
7179
return (V) new Invocation(ctx, call).proceed(args);
7280
}
7381
}
7482

83+
/**
84+
* The degenerate case for {@link #mergeAndCollapse(List[])}. This is here only to eliminate the unchecked varargs compiler
85+
* warnings that would otherwise be issued in code that does not have any interceptors on a method.
86+
*
87+
* @param <T> the type of the provider
88+
* @return an empty list
89+
* @deprecated this method should only be called by generated code
90+
*/
91+
@Deprecated
92+
public static <T> List<Provider<T>> mergeAndCollapse() {
93+
return List.of();
94+
}
95+
7596
/**
7697
* Merges a variable number of lists together, where the net result is the merged set of non-null providers
7798
* ranked in proper weight order, or else empty list.
7899
*
79100
* @param lists the lists to merge
80101
* @param <T> the type of the provider
81-
* @return the merged result, or null instead of empty lists
102+
* @return the merged result or empty list if there is o interceptor providers
82103
*/
83104
@SuppressWarnings("unchecked")
84105
public static <T> List<Provider<T>> mergeAndCollapse(List<Provider<T>>... lists) {
@@ -118,19 +139,52 @@ public static <T> List<Provider<T>> mergeAndCollapse(List<Provider<T>>... lists)
118139

119140
@Override
120141
public V proceed(Object... args) {
121-
if (interceptorIterator.hasNext()) {
122-
return interceptorIterator.next()
123-
.get()
124-
.proceed(ctx, this, args);
125-
} else {
126-
if (this.call != null) {
127-
Function<Object[], V> call = this.call;
128-
this.call = null;
129-
return call.apply(args);
130-
} else {
131-
throw new IllegalStateException("Duplicate invocation, or unknown call type: " + this);
142+
if (this.call == null) {
143+
throw new InvocationException("Duplicate invocation, or unknown call type: " + this, true);
144+
}
145+
146+
if (interceptorPos < interceptors.size()) {
147+
Provider<Interceptor> interceptorProvider = interceptors.get(interceptorPos);
148+
Interceptor interceptor = interceptorProvider.get();
149+
interceptorPos++;
150+
try {
151+
return interceptor.proceed(ctx, this, args);
152+
} catch (Throwable t) {
153+
interceptorPos--;
154+
155+
if (t instanceof InvocationException) {
156+
throw t;
157+
}
158+
159+
throw (interceptorProvider instanceof ServiceProvider)
160+
? new InvocationException("Error in interceptor chain processing",
161+
t,
162+
(ServiceProvider<?>) interceptorProvider,
163+
call == null)
164+
: new InvocationException("Error in interceptor chain processing",
165+
t,
166+
call == null);
132167
}
133168
}
169+
170+
Function<Object[], V> call = this.call;
171+
this.call = null;
172+
173+
try {
174+
return call.apply(args);
175+
} catch (Throwable t) {
176+
if (t instanceof InvocationException) {
177+
if (!((InvocationException) t).targetWasCalled()) {
178+
// allow the call to happen again
179+
this.call = call;
180+
}
181+
throw t;
182+
}
183+
184+
// allow the call to happen again
185+
this.call = call;
186+
throw new InvocationException("Error in interceptor chain processing", t, true);
187+
}
134188
}
135189

136190
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.pico.runtime;
18+
19+
import java.util.Optional;
20+
21+
import io.helidon.builder.Builder;
22+
import io.helidon.config.metadata.ConfiguredOption;
23+
24+
@Builder
25+
abstract class Control {
26+
abstract Optional<RuntimeException> exceptionBeforeProceed();
27+
abstract Optional<RuntimeException> exceptionAfterProceed();
28+
abstract Optional<Object> shortCircuitValue();
29+
abstract int timesToCatchException();
30+
@ConfiguredOption("1") abstract int timesToCallProceed();
31+
}

0 commit comments

Comments
 (0)