001package com.randomnoun.common;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.util.*;
008
009
010/**
011 * Can probably do all this with ThreadLocals these days.
012 * 
013 * <p>Manages a set of hashmaps used by EJB/servlet containers to hold <b>thread-global</b> data.
014 * 
015 * <p>For example, calling <code>ThreadContext.get("asd")</code> from two different threads may
016 * return two different values.
017 *
018 * <p>In addition, each thread is supplied with a stack context which allows multiple EJBs
019 * to run in the same thread (e.g. if one EJB invokes another local EJB which then
020 * executes within the same thread). To ensure separation of data, each EJB
021 * should invoke {@link #push()} to create its own context as soon as
022 * it is invoked, and call {@link #pop()} before it terminates to
023 * maintain the per-thread data stack. (i.e. the invoked method, not the method caller is
024 * responsible for maintaining data integrity).
025 *
026 * <p>This class has all the methods of {@link java.util.Map}, although these are
027 * now static. A real thread-specific java.util.Map object can be obtained by
028 * calling the {@link #getMap() getMap} method. Using this object rather than
029 * calling static methods on ThreadContext directly may improve performance. The
030 * map instance returned by getMap() is <b>not</b> synchronized, since it is
031 * assumed that only one thread is accessing it's own context at the same time.
032 *
033 * <p>Each EJB (or thread) wishing to use this data structure should clean up after
034 * itself by invoking ThreadContext.clear() before passing control back to the
035 * EJB container (or terminating). Every thread MUST be removed using .pop(), otherwise
036 * memory leaks will occur.
037 *
038 * <p>Implementation note: this class relies on different thread's returning unique
039 * strings for their Thread.getName() method. It will break if this is not the
040 * case in a particular VM implementation. 
041 *
042 * <p><b>NB:</b> Don't use this method to pass state between EJBs. 
043 * Where an EJB invokes a second EJB, it should be
044 * assumed that the invoked EJB exists on another VM. Since this data
045 * structure will not be populated correctly on the second VM, it should not
046 * be relied upon to pass state information between EJBs.
047 *
048 * @author knoxg
049 * 
050 */
051public class ThreadContext
052{
053    /** Backing map of threads-IDs to thread-specific Lists, containing maps */
054    private static Map<String, List<Map<Object, Object>>> globalMap;
055
056    /**
057     * Creates a new ThreadContext object.
058     *
059     * @throws Exception DOCUMENT ME!
060     */
061    public ThreadContext()
062        throws Exception
063    {
064        /** VM singleton - cannot be instantiated */
065        throw new Exception("Cannot instantiate ThreadContext");
066    }
067
068    /** Create a new context for this thread.
069     *
070     *  @return The global map for the newly created context.
071     **/
072    public static Map<Object, Object> push()
073    {
074        List<Map<Object,Object>> list;
075        Map<Object, Object> map;
076        String currentThreadName = Thread.currentThread().getName();
077
078        synchronized (globalMap) {
079            list = (List<Map<Object,Object>>) globalMap.get(currentThreadName);
080
081            if (list == null) {
082                list = new ArrayList<Map<Object,Object>>();
083                globalMap.put(currentThreadName, list);
084            }
085
086            map = new HashMap<Object, Object>();
087            list.add(map);
088        }
089
090        return map;
091    }
092
093    /** Remove the most newly-created context for this thread.
094     *
095     *  @throws IllegalStateException This exception is thrown if no context
096     *    exists for this thread.
097     */
098    public static void pop()
099    {
100        // NB: Lists are never removed from the global hashmap, as we presume
101        // that threads are reused by the server container. This may prevent
102        // this class from being used in a more general-purpose (non-EJB-container)
103        // solution.
104        List<Map<Object,Object>> list;
105        String currentThreadName = Thread.currentThread().getName();
106
107        synchronized (globalMap)
108        {
109            list = globalMap.get(currentThreadName);
110
111            if (list == null) {
112                throw new IllegalStateException("No context exists for this thread.");
113            }
114
115            if (list.size() == 0) {
116                throw new IllegalStateException("No context exists for this thread.");
117            }
118
119            list.remove(list.size() - 1);
120
121            if (list.size() == 0) {
122                globalMap.remove(currentThreadName);
123            }
124        }
125    }
126
127    /** Returns a java.util.Map object which contains all thread-specific
128     * data. Using the returned object from this method repeatedly
129     * from within a single calling method will be more efficient than calling
130     * the static methods of this class.
131     *
132     * @return The map for this thread.
133     * @throws IllegalStateException This exception is thrown if a per-thread
134     *   context has not yet been created by calling push().
135     */
136    public static Map<Object, Object> getMap()
137    {
138        List<Map<Object,Object>> list;
139        String currentThreadName = Thread.currentThread().getName();
140
141        list = globalMap.get(currentThreadName);
142
143        if (list == null) {
144            list = new ArrayList<Map<Object,Object>>();
145            globalMap.put(currentThreadName, list);
146        }
147
148        // create an exception if one does not exist
149        if (list.size() == 0)         {
150            throw new IllegalStateException(
151                "ThreadContext has not yet been created (push() must be invoked before getMap())");
152        }
153
154        return list.get(list.size() - 1);
155    }
156
157    /** Returns true if a ThreadContext has been set up for the current Thread,
158     * (through a push() call), false otherwise.
159     *
160     * @return true if a ThreadContext exists, false otherwise
161     */
162    public static boolean contextExists()
163    {
164        List<Map<Object,Object>> list;
165        String currentThreadName = Thread.currentThread().getName();
166
167        list = globalMap.get(currentThreadName);
168
169        return (list != null) && (list.size() > 0);
170    }
171
172    // ***************************************************************************************
173    //
174    // All methods below this line mirror the methods within java.util.Map, but are static,
175    // and operate on the current thread context.
176    //
177
178    /** Clears the current EJB's map. This method only clears out the existing thread context,
179     *  it does not pop the current context off the stack
180     */
181    public static void clear()
182    {
183        getMap().clear();
184    }
185
186    /** As per {@link java.util.Map#containsKey(java.lang.Object)} for the current thread's context
187     * 
188     * @return true if this map contains a mapping for the specified key
189     *
190     * @throws IllegalStateException This exception is thrown if a per-thread
191     *   context has not yet been created by calling push().
192     */
193    public static boolean containsKey(Object key)
194    {
195        return getMap().containsKey(key);
196    }
197
198    /** As per {@link java.util.Map#containsValue(java.lang.Object)} for the current thread's context
199     *  
200     * @return true if this map maps one or more keys to the specified value. 
201     *
202     * @throws IllegalStateException This exception is thrown if a per-thread
203     *   context has not yet been created by calling push().
204     */
205    public static boolean containsValue(Object value)
206    {
207        return getMap().containsValue(value);
208    }
209
210    /**
211     * As per {@link java.util.Map#entrySet()} for the current thread's context
212     *
213     * @return a set view of the mappings contained in this map.
214     *
215     * @throws IllegalStateException This exception is thrown if a per-thread
216     *   context has not yet been created by calling push().
217     */
218    public static Set<Map.Entry<Object, Object>> entrySet()
219    {
220        return getMap().entrySet();
221    }
222
223    /**
224     * As per {@link java.util.Map#get(java.lang.Object)} for the current thread's context
225     *
226     * @param key key whose associated value is to be returned. 
227
228     *
229     * @return the value to which this map maps the specified key, or null if the map contains 
230     *   no mapping for this key
231     *
232     * @throws IllegalStateException This exception is thrown if a per-thread
233     *   context has not yet been created by calling push().
234     */
235    public static Object get(Object key)
236    {
237        return getMap().get(key);
238    }
239
240    /**
241     * As per {@link java.util.Map#isEmpty()} for the current thread's context
242     *
243     * @return true if this map contains no key-value mappings
244     *
245     * @throws IllegalStateException This exception is thrown if a per-thread
246     *   context has not yet been created by calling push().
247     */
248    public static boolean isEmpty()
249    {
250        return getMap().isEmpty();
251    }
252
253    /**
254     * As per {@link java.util.Map#keySet()} for the current thread's context
255     *
256     * @return a set view of the keys contained in this map
257     *
258     * @throws IllegalStateException This exception is thrown if a per-thread
259     *   context has not yet been created by calling push().
260     */
261    public static Set<Object> keySet()
262    {
263        return getMap().keySet();
264    }
265
266    /**
267     * As per {@link java.util.Map#put(java.lang.Object, java.lang.Object)} for the current thread's 
268     * context
269     *
270     * @param key key with which the specified value is to be associated
271     * @param value value to be associated with the specified key
272     *
273     * @return previous value associated with specified key, or null if there was no mapping for key. A null return can also indicate that the map previously associated null 
274     *   with the specified key, if the implementation supports null values
275     *
276     * @throws IllegalStateException This exception is thrown if a per-thread
277     *   context has not yet been created by calling push().
278     */
279    public static Object put(Object key, Object value)
280    {
281        return getMap().put(key, value);
282    }
283
284    /**
285     * As per {@link java.util.Map#putAll(java.util.Map)} for the current thread's context
286     *
287     * @param t Mappings to be stored in this map
288     *
289     * @throws IllegalStateException This exception is thrown if a per-thread
290     *   context has not yet been created by calling push().
291     */
292    public static void putAll(Map<Object, Object> t)
293    {
294        getMap().putAll(t);
295    }
296
297    /**
298     * As per {@link java.util.Map#remove(java.lang.Object)} for the current thread's context
299     *
300     * @param key key whose mapping is to be removed from the map
301     *
302     * @return previous value associated with specified key, or null if there was no mapping for key
303     *
304     * @throws IllegalStateException This exception is thrown if a per-thread
305     *   context has not yet been created by calling push().
306     */
307    public static Object remove(Object key)
308    {
309        return getMap().remove(key);
310    }
311
312    /**
313     * As per {@link java.util.Map#size()} for the current thread's context
314     *
315     * @return the number of key-value mappings in this map
316     *
317     * @throws IllegalStateException This exception is thrown if a per-thread
318     *   context has not yet been created by calling push().
319     */
320    public static int size()
321    {
322        return getMap().size();
323    }
324
325    /**
326     * As per {@link java.util.Map#values()} for the current thread's context
327     *
328     * @return a collection view of the values contained in this map
329     *
330     * @throws IllegalStateException This exception is thrown if a per-thread
331     *   context has not yet been created by calling push().
332     */
333    public static Collection<Object> values()
334    {
335        return getMap().values();
336    }
337
338    static
339    {
340        // set up the globalMap containing all ThreadContexts
341        globalMap = Collections.synchronizedMap(new HashMap<String, List<Map<Object, Object>>>());
342    }
343}