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}