Skip to content

Commit aac3d76

Browse files
authored
Introduce ContextObserver API (Merge #92)
2 parents 3ff3734 + 1582e43 commit aac3d76

File tree

22 files changed

+724
-38
lines changed

22 files changed

+724
-38
lines changed

context-propagation-java5/src/main/java/nl/talsmasoftware/context/Context.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 Talsma ICT
2+
* Copyright 2016-2019 Talsma ICT
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,9 @@
2020
/**
2121
* A context can be anything that needs to be maintained on the 'current thread' level.
2222
* <p>
23+
* It is the responsibility of the one activating a new context to
24+
* also {@linkplain #close() close} it again <em>from the same thread</em>.
25+
* <p>
2326
* Implementations are typically maintained within a static {@link ThreadLocal} variable.<br>
2427
* A context has a very simple life-cycle: they can be created and {@link #close() closed}.
2528
* A well-behaved <code>Context</code> implementation will make sure that thread-local state is restored
@@ -48,15 +51,17 @@ public interface Context<T> extends Closeable {
4851
T getValue();
4952

5053
/**
51-
* Closes this context and restores any context changes made by this object to the way things were before it
52-
* got created.
54+
* Closes this context.
55+
* <p>
56+
* It is the responsibility of the one activating a new context to also close it again <em>from the same thread</em>.
5357
* <p>
5458
* It must be possible to call this method multiple times.
5559
* It is the responsibility of the implementor of this context to make sure that closing an already-closed context
5660
* has no unwanted side-effects.
5761
* A simple way to achieve this is by using an {@link java.util.concurrent.atomic.AtomicBoolean} to make sure the
5862
* 'closing' transition is executed only once.
5963
* <p>
64+
* Implementors should attempt to restore previous contextual state upon close.
6065
*
6166
* @throws RuntimeException if an error occurs while restoring the context.
6267
*/

context-propagation-java5/src/main/java/nl/talsmasoftware/context/ContextManager.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 Talsma ICT
2+
* Copyright 2016-2019 Talsma ICT
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,12 +18,20 @@
1818
/**
1919
* The contract for a ContextManager Service.
2020
* <p>
21-
* Concrete implementations can be registered by providing an implementation class with a default constructor,
22-
* along with a class declaration in a service file called:<br>
23-
* <code>"/META-INF/services/nl.talsmasoftware.context.ContextManager"</code>
24-
* <p>
21+
* Implementations can be registered by providing a fully qualified class name in a service file called:<br>
22+
* <code>"/META-INF/services/nl.talsmasoftware.context.ContextManager"</code><br>
2523
* That will take care of any active context being captured in {@link ContextSnapshot} instances
26-
* managed by the {@link ContextManagers} utility class.
24+
* managed by the {@link ContextManagers} utility class.<br>
25+
* <b>Note:</b> <em>Make sure your implementation has a default (no-argument) constructor.</em>
26+
* <p>
27+
* A context manager is required to notify
28+
* registered {@linkplain nl.talsmasoftware.context.observer.ContextObserver ContextObserver}
29+
* of context updates.
30+
* Using the {@linkplain nl.talsmasoftware.context.threadlocal.AbstractThreadLocalContext AbstractThreadLocalContext}
31+
* already fulfills this requirement.
32+
* Other implementations can use the utility methods defined in
33+
* {@linkplain nl.talsmasoftware.context.observer.ContextObservers} to locate the appropriate context observers
34+
* to be notified.
2735
*
2836
* @author Sjoerd Talsma
2937
*/

context-propagation-java5/src/main/java/nl/talsmasoftware/context/PriorityServiceLoader.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 Talsma ICT
2+
* Copyright 2016-2019 Talsma ICT
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@
3737
* @param <SVC> The type of service to load.
3838
* @author Sjoerd Talsma
3939
*/
40-
final class PriorityServiceLoader<SVC> implements Iterable<SVC> {
40+
public final class PriorityServiceLoader<SVC> implements Iterable<SVC> {
4141
private static final Logger LOGGER = Logger.getLogger(PriorityServiceLoader.class.getName());
4242
private static final String SYSTEMPROPERTY_CACHING = "talsmasoftware.context.caching";
4343
private static final String ENVIRONMENT_CACHING_VALUE = System.getenv(
@@ -46,7 +46,7 @@ final class PriorityServiceLoader<SVC> implements Iterable<SVC> {
4646
private final Class<SVC> serviceType;
4747
private final Map<ClassLoader, List<SVC>> cache = new WeakHashMap<ClassLoader, List<SVC>>();
4848

49-
PriorityServiceLoader(Class<SVC> serviceType) {
49+
public PriorityServiceLoader(Class<SVC> serviceType) {
5050
if (serviceType == null) throw new NullPointerException("Service type is <null>.");
5151
this.serviceType = serviceType;
5252
}
@@ -62,18 +62,18 @@ public Iterator<SVC> iterator() {
6262
return services.iterator();
6363
}
6464

65-
private static boolean isCachingDisabled() {
66-
final String cachingProperty = System.getProperty(SYSTEMPROPERTY_CACHING, ENVIRONMENT_CACHING_VALUE);
67-
return "0".equals(cachingProperty) || "false".equalsIgnoreCase(cachingProperty);
68-
}
69-
7065
/**
7166
* Removes the cache so the next call to {@linkplain #iterator()} will attempt to load the objects again.
7267
*/
73-
void clearCache() {
68+
public void clearCache() {
7469
cache.clear();
7570
}
7671

72+
private static boolean isCachingDisabled() {
73+
final String cachingProperty = System.getProperty(SYSTEMPROPERTY_CACHING, ENVIRONMENT_CACHING_VALUE);
74+
return "0".equals(cachingProperty) || "false".equalsIgnoreCase(cachingProperty);
75+
}
76+
7777
private List<SVC> findServices(ClassLoader classLoader) {
7878
ArrayList<SVC> found = new ArrayList<SVC>();
7979
for (Iterator<SVC> iterator = loadServices(serviceType, classLoader); iterator.hasNext(); ) {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2016-2019 Talsma ICT
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+
package nl.talsmasoftware.context.observer;
17+
18+
import nl.talsmasoftware.context.ContextManager;
19+
20+
/**
21+
* Observe context updates for a particular class of {@linkplain ContextManager}.
22+
* <p>
23+
* Create your context observer by implementing this interface
24+
* and registering your class to the {@code ServiceLoader}
25+
* SPI by adding the fully qualified class name to the resource
26+
* {@code /META-INF/services/nl.talsmasoftware.context.observer.ContextObserver}.
27+
* <p>
28+
* It is the responsibility of the context implementor to update observers of
29+
* context updates. SPI lookup of appropriate observers is facilitated
30+
* by the {@linkplain ContextObservers#onActivate(Class, Object, Object)}
31+
* and {@linkplain ContextObservers#onDeactivate(Class, Object, Object)}
32+
* utility methods.<br>
33+
* All subclasses of {@code AbstractThreadLocalContext} are already observable.
34+
*
35+
* @author Sjoerd Talsma
36+
*/
37+
public interface ContextObserver<T> {
38+
39+
/**
40+
* The observed context manager(s).
41+
* <p>
42+
* Context observers can indicate which type of context manager must be observed using this method.
43+
* For instance, returning {@code LocaleContextManager.class} here, updates for the {@code LocaleContext} will
44+
* be offered to this observer.
45+
* <p>
46+
* To observe <em>all</em> context updates, return the {@linkplain ContextManager} interface class itself,
47+
* since all context managers must implement it.
48+
* <p>
49+
* Return {@code null} to disable the observer.
50+
*
51+
* @return The observed context manager class or {@code null} to disable this observer.
52+
*/
53+
Class<? extends ContextManager<T>> getObservedContextManager();
54+
55+
/**
56+
* Indicates that a context <em>was just activated</em>.
57+
*
58+
* @param activatedContextValue The now active context value.
59+
* @param previousContextValue The previous context value or {@code null} if unknown or unsupported.
60+
*/
61+
void onActivate(T activatedContextValue, T previousContextValue);
62+
63+
/**
64+
* Indicates that a context <em>was just deactivated</em>.
65+
*
66+
* @param deactivatedContextValue The deactivated context value.
67+
* @param restoredContextValue The now active restored context value or {@code null} if unknown or unsupported.
68+
*/
69+
void onDeactivate(T deactivatedContextValue, T restoredContextValue);
70+
71+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2016-2019 Talsma ICT
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+
package nl.talsmasoftware.context.observer;
17+
18+
import nl.talsmasoftware.context.ContextManager;
19+
import nl.talsmasoftware.context.PriorityServiceLoader;
20+
21+
import java.util.logging.Level;
22+
import java.util.logging.Logger;
23+
24+
/**
25+
* Utility class to assist Context implementors.
26+
* <p>
27+
* It implements the SPI behaviour, locating appropriate {@linkplain ContextObserver}
28+
* implementations to be notified of {@linkplain #onActivate(Class, Object, Object) activate}
29+
* and {@linkplain #onDeactivate(Class, Object, Object) deactivate} occurrances.
30+
*
31+
* @author Sjoerd Talsma
32+
*/
33+
public final class ContextObservers {
34+
35+
/**
36+
* The service loader that loads (and possibly caches) {@linkplain ContextManager} instances in prioritized order.
37+
*/
38+
private static final PriorityServiceLoader<ContextObserver> CONTEXT_OBSERVERS =
39+
new PriorityServiceLoader<ContextObserver>(ContextObserver.class);
40+
41+
/**
42+
* Private constructor to avoid instantiation of this class.
43+
*/
44+
private ContextObservers() {
45+
throw new UnsupportedOperationException();
46+
}
47+
48+
/**
49+
* Notifies all {@linkplain ContextObserver context observers} for the specified {@code contextManager}
50+
* about the activated context value.
51+
*
52+
* @param contextManager The context manager type that activated the context (required to observe).
53+
* @param activatedContextValue The activated context value or {@code null} if no value was activated.
54+
* @param previousContextValue The previous context value or {@code null} if unknown or unsupported.
55+
* @param <T> The type managed by the context manager.
56+
*/
57+
@SuppressWarnings("unchecked") // If the observer tells us it can observe the values, we trust it.
58+
public static <T> void onActivate(Class<? extends ContextManager<? super T>> contextManager,
59+
T activatedContextValue,
60+
T previousContextValue) {
61+
if (contextManager != null) for (ContextObserver observer : CONTEXT_OBSERVERS) {
62+
try {
63+
final Class observedContext = observer.getObservedContextManager();
64+
if (observedContext != null && observedContext.isAssignableFrom(contextManager)) {
65+
observer.onActivate(activatedContextValue, previousContextValue);
66+
}
67+
} catch (RuntimeException observationException) {
68+
Logger.getLogger(observer.getClass().getName()).log(Level.WARNING,
69+
"Exception in " + observer.getClass().getSimpleName()
70+
+ ".onActivate(" + activatedContextValue + ", " + previousContextValue
71+
+ ") for " + contextManager.getSimpleName() + ": " + observationException.getMessage(),
72+
observationException);
73+
}
74+
}
75+
}
76+
77+
/**
78+
* Notifies all {@linkplain ContextObserver context observers} for the specified {@code contextManager}
79+
* about the deactivated context value.
80+
*
81+
* @param contextManager The context manager type that deactivated the context (required to observe).
82+
* @param deactivatedContextValue The deactivated context value
83+
* @param restoredContextValue The restored context value or {@code null} if unknown or unsupported.
84+
* @param <T> The type managed by the context manager.
85+
*/
86+
@SuppressWarnings("unchecked") // If the observer tells us it can observe the values, we trust it.
87+
public static <T> void onDeactivate(Class<? extends ContextManager<? super T>> contextManager,
88+
T deactivatedContextValue,
89+
T restoredContextValue) {
90+
if (contextManager != null) for (ContextObserver observer : CONTEXT_OBSERVERS) {
91+
try {
92+
final Class observedContext = observer.getObservedContextManager();
93+
if (observedContext != null && observedContext.isAssignableFrom(contextManager)) {
94+
observer.onDeactivate(deactivatedContextValue, restoredContextValue);
95+
}
96+
} catch (RuntimeException observationException) {
97+
Logger.getLogger(observer.getClass().getName()).log(Level.WARNING,
98+
"Exception in " + observer.getClass().getSimpleName()
99+
+ ".onDeactivate(" + deactivatedContextValue + ", " + deactivatedContextValue
100+
+ ") for " + contextManager.getSimpleName() + ": " + observationException.getMessage(),
101+
observationException);
102+
}
103+
}
104+
}
105+
106+
}

context-propagation-java5/src/main/java/nl/talsmasoftware/context/threadlocal/AbstractThreadLocalContext.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 Talsma ICT
2+
* Copyright 2016-2019 Talsma ICT
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616
package nl.talsmasoftware.context.threadlocal;
1717

1818
import nl.talsmasoftware.context.Context;
19+
import nl.talsmasoftware.context.ContextManager;
20+
import nl.talsmasoftware.context.observer.ContextObservers;
1921

2022
import java.lang.reflect.Modifier;
2123
import java.util.concurrent.ConcurrentHashMap;
@@ -32,6 +34,7 @@
3234
* @author Sjoerd Talsma
3335
*/
3436
public abstract class AbstractThreadLocalContext<T> implements Context<T> {
37+
private static final AtomicBoolean DEPRECATED_CONSTRUCTOR_WARNING = new AtomicBoolean(true);
3538
/**
3639
* The constant of ThreadLocal context instances per subclass name so different types don't get mixed.
3740
*/
@@ -42,6 +45,7 @@ public abstract class AbstractThreadLocalContext<T> implements Context<T> {
4245
private final ThreadLocal<AbstractThreadLocalContext<T>> sharedThreadLocalContext = threadLocalInstanceOf((Class) getClass());
4346
private final Logger logger = Logger.getLogger(getClass().getName());
4447
private final AtomicBoolean closed = new AtomicBoolean(false);
48+
private final Class<? extends ContextManager<? super T>> contextManagerType;
4549

4650
/**
4751
* The parent context that was active at the time this context was created (if any)
@@ -64,14 +68,36 @@ public abstract class AbstractThreadLocalContext<T> implements Context<T> {
6468
*
6569
* @param newValue The new value to become active in this new context
6670
* (or <code>null</code> to register a new context with 'no value').
71+
* @deprecated Using this constructor makes it impossible to register a {@code ContextObserver}!
6772
*/
68-
@SuppressWarnings("unchecked")
73+
@Deprecated
6974
protected AbstractThreadLocalContext(T newValue) {
75+
this(null, newValue);
76+
logger.log(DEPRECATED_CONSTRUCTOR_WARNING.compareAndSet(true, false) ? Level.WARNING : Level.FINE,
77+
"Initialized new {0} without context manager type. " +
78+
"This makes it impossible to register ContextObservers for it. " +
79+
"Please fix {1} by specifying a ContextManager type " +
80+
"in the AbstractThreadLocalContext constructor.",
81+
new Object[]{this, getClass().getSimpleName()});
82+
}
83+
84+
/**
85+
* Instantiates a new context with the specified value.
86+
* The new context will be made the active context for the current thread.
87+
*
88+
* @param contextManagerType The context manager type (required to notify appropriate observers)
89+
* @param newValue The new value to become active in this new context
90+
* (or <code>null</code> to register a new context with 'no value').
91+
*/
92+
@SuppressWarnings("unchecked")
93+
protected AbstractThreadLocalContext(Class<? extends ContextManager<? super T>> contextManagerType, T newValue) {
7094
this.unwindIfNecessary(); // avoid unnecessary parentContexts
95+
this.contextManagerType = contextManagerType;
7196
this.parentContext = sharedThreadLocalContext.get();
7297
this.value = newValue;
7398
this.sharedThreadLocalContext.set(this);
7499
logger.log(Level.FINEST, "Initialized new {0}.", this);
100+
ContextObservers.onActivate(contextManagerType, value, parentContext == null ? null : parentContext.getValue());
75101
}
76102

77103
/**
@@ -119,9 +145,12 @@ public T getValue() {
119145
* This method has no side-effects if the context was already closed (it is safe to call multiple times).
120146
*/
121147
public void close() {
122-
closed.set(true);
123-
this.unwindIfNecessary(); // Remove this context created in the same thread.
148+
final boolean observe = closed.compareAndSet(false, true);
149+
final Context<T> restored = this.unwindIfNecessary(); // Remove this context created in the same thread.
124150
logger.log(Level.FINEST, "Closed {0}.", this);
151+
if (observe) {
152+
ContextObservers.onDeactivate(contextManagerType, this.value, restored == null ? null : restored.getValue());
153+
}
125154
}
126155

127156
/**

context-propagation-java5/src/test/java/nl/talsmasoftware/context/DummyContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2017 Talsma ICT
2+
* Copyright 2016-2019 Talsma ICT
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ final class DummyContext extends AbstractThreadLocalContext<String> {
2525
AbstractThreadLocalContext.threadLocalInstanceOf(DummyContext.class);
2626

2727
DummyContext(String newValue) {
28-
super(newValue);
28+
super(DummyContextManager.class, newValue);
2929
}
3030

3131
// Public for testing!

0 commit comments

Comments
 (0)