001package com.identityworksllc.iiq.common;
002
003import sailpoint.server.Environment;
004
005import java.util.function.Supplier;
006
007/**
008 * A thread-local container that is associated with the plugin cache version. If
009 * a new plugin is installed, the version increments, and the entire ThreadLocal
010 * will be replaced. This means that running processes relying on plugin-provided
011 * classes can retrieve new versions of those objects.
012 *
013 * Your {@link Supplier} code should always assume that the plugin cache has
014 * been refreshed since the last invocation and never cache plugin objects.
015 *
016 * The object type T, of course, cannot be the actual implementation
017 * in the plugin classloader, as that class will have become invalid when the
018 * plugin cache was reset. The type T should be either a JDK class or a custom
019 * interface implemented at the webapp layer.
020 *
021 * @param <T> the type of the value held in this ThreadLocal
022 */
023public class VersionedThreadLocal<T> implements Supplier<T> {
024
025    /**
026     * Creates a new VersionedThreadLocal with the given initial value.
027     * @param supplier the initial value supplier
028     * @return a new VersionedThreadLocal
029     * @param <U> the type of the value held in this ThreadLocal
030     *
031     * @see ThreadLocal#withInitial(Supplier) 
032     */
033    public static <U> VersionedThreadLocal<U> withInitial(Supplier<? extends U> supplier) {
034        return new VersionedThreadLocal<>(supplier);
035    }
036
037    /**
038     * The supplier that provides the initial value for this ThreadLocal.
039     */
040    private final Supplier<? extends T> supplier;
041
042    /**
043     * The ThreadLocal instance that holds the value. This will be replaced on the
044     * next call to {@link #get()} or {@link #set} when the value changes.
045     */
046    private ThreadLocal<T> threadLocal;
047
048    /**
049     * The version of the plugin cache, used to determine if the threadlocal needs
050     * to be replaced.
051     */
052    private int version;
053
054    /**
055     * Creates a new VersionedThreadLocal with the default initial value of null.
056     */
057    public VersionedThreadLocal() {
058        this(() -> null);
059    }
060
061    /**
062     * Creates a new VersionedThreadLocal with the given initial value.
063     *
064     * @param supplier the initial value supplier
065     */
066    private VersionedThreadLocal(Supplier<? extends T> supplier) {
067        this.supplier = supplier;
068        this.threadLocal = ThreadLocal.withInitial(supplier);
069        this.version = Environment.getEnvironment().getPluginsCache().getVersion();
070    }
071
072    /**
073     * Computes the value for this ThreadLocal using the given supplier. If the
074     * value is already set, it will be returned. Otherwise, the supplier will be
075     * called to compute the value and set it in this ThreadLocal.
076     *
077     * @param supplier the supplier to compute the value
078     * @return the current or computed value
079     */
080    public T compute(Supplier<? extends T> supplier) {
081        T value = get();
082        if (value == null) {
083            value = supplier.get();
084            set(value);
085        }
086        return value;
087    }
088
089    /**
090     * Returns the current value in this ThreadLocal. If the version has changed,
091     * a new ThreadLocal will be created and the value returned from that.
092     *
093     * @return the current value in this ThreadLocal
094     * @see ThreadLocal#get()
095     */
096    @Override
097    public T get() {
098        int currentVersion = Environment.getEnvironment().getPluginsCache().getVersion();
099        if (currentVersion != version) {
100            synchronized (this) {
101                version = currentVersion;
102                threadLocal = ThreadLocal.withInitial(supplier);
103            }
104        }
105
106        return threadLocal.get();
107    }
108
109    /**
110     * Sets the value in this ThreadLocal. If the version has changed, a new
111     * ThreadLocal will be created and the value set in that.
112     *
113     * @param value the new value to set
114     * @see ThreadLocal#set(Object)
115     */
116    public void set(T value) {
117        int currentVersion = Environment.getEnvironment().getPluginsCache().getVersion();
118        if (currentVersion != version) {
119            synchronized(this) {
120                version = currentVersion;
121                threadLocal = ThreadLocal.withInitial(supplier);
122            }
123        }
124
125        threadLocal.set(value);
126    }
127
128}