/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file.
*/
package er.extensions.foundation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WOSession;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSSet;
import er.extensions.appserver.ERXApplication;
import er.extensions.concurrency.ERXCloneableThreadLocal;
import er.extensions.eof.ERXEOControlUtilities;
/**
* Provides a way to store objects for a particular thread. This can be especially handy for storing objects
* like the current actor or the current form name within the scope of a thread handling a particular request.
* <p>
* The system property <code>er.extensions.ERXThreadStorage.useInheritableThreadLocal</code> defines if the
* thread storage can be either inherited by client threads (default) or get used only by the current thread.
* The usage of some types of objects inherited from the parent thread can cause problems.
* </p><p>
* The system property <code>er.extensions.ERXThreadStorage.logUsageOfProblematicInheritedValues</code>
* defines, if potential problems should be logged. This defaults to <code>true</code> when running in development mode
* and to <code>false</code> when running a deployed application.
*/
public class ERXThreadStorage {
private static final Logger log = Logger.getLogger(ERXThreadStorage.class);
public static final String KEYS_ADDED_IN_CURRENT_THREAD_KEY = "ERXThreadStorage.keysAddedInCurrentThread";
public static final String WAS_CLONED_MARKER = "ERXThreadStorage.wasCloned";
private static Set<Class<?>> _problematicTypes ;
private static Set<String> _problematicKeys;
/** Holds the single instance of the thread map. */
private static ThreadLocal threadMap;
private static Boolean _useInheritableThreadLocal;
private static Boolean _logUsageOfProblematicInheritedValues;
static {
if(useInheritableThreadLocal()) {
threadMap = new ERXThreadStorageCloneableThreadLocal();
} else {
threadMap = new ThreadLocal();
}
_problematicTypes = new NSSet<Class<?>>(
new Class[] {
WOSession.class,
WOContext.class,
EOEnterpriseObject.class,
EOEditingContext.class
}
);
_problematicKeys = new NSSet<String>(
new String[] {
// already handled by "_problematcTypes"
// ERXWOContext.CONTEXT_DICTIONARY_KEY
}
);
}
/**
* Checks the system property <code>er.extensions.ERXThreadStorage.useInheritableThreadLocal</code>
* to decide whether to use inheritable thread variables or not.
* @return true if set (default)
*/
private static boolean useInheritableThreadLocal() {
if (_useInheritableThreadLocal == null) {
_useInheritableThreadLocal = Boolean.valueOf(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXThreadStorage.useInheritableThreadLocal", true));
}
return _useInheritableThreadLocal.booleanValue();
}
/**
* Checks the system property <code>er.extensions.ERXThreadStorage.logUsageOfProblematicInheritedValues</code>
* to decide whether to log potential problems when using certain values inherited by the parent thread.
* Only applies if using inheritable thread variables.
* @return true if set (default)
*/
private static boolean logUsageOfProblematicInheritedValues() {
if (_logUsageOfProblematicInheritedValues == null) {
boolean devMode = ERXApplication.isDevelopmentModeSafe();
_logUsageOfProblematicInheritedValues = Boolean.valueOf(useInheritableThreadLocal() && ERXProperties.booleanForKeyWithDefault("er.extensions.ERXThreadStorage.logUsageOfProblematicInheritedValues", devMode));
}
return _logUsageOfProblematicInheritedValues.booleanValue();
}
/** Holds the default initialization value of the hash map. */
private static int DefaultHashMapSize = 10;
/**
* Sets a value for a particular key for a particular thread.
* @param object value
* @param key key
*/
public static void takeValueForKey(Object object, String key) {
// log.debug(key+" <- "+object);
Map map = storageMap(true);
map.put(key, object);
markKeyAddedInCurrentThread(key);
}
/**
* Removes the value in the map for a given key.
* @param key key to be removed from the map.
* @return the object corresponding to the key that
* was removed, null if nothing is found.
*/
public static Object removeValueForKey(String key) {
Map map = storageMap(false);
return map != null ? map.remove(key) : null;
}
/**
* Gets the object associated with the keypath in the storage
* map off of the current thread.
*
* @param keyPath key path to be used to retrieve value from map.
* @return the value stored in the map for the given key.
*/
public static Object valueForKeyPath(String keyPath) {
int dot = keyPath.indexOf(".");
Object value = null;
if(dot > 1) {
value = valueForKey(keyPath.substring(0, dot));
if(value != null) {
value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(value, keyPath.substring(dot+1));
}
} else {
value = valueForKey(keyPath);
}
return value;
}
/**
* Gets the object associated with the key in the storage
* map off of the current thread.
* @param key key to be used to retrieve value from map.
* @return the value stored in the map for the given key.
*/
public static Object valueForKey(String key) {
Map map = storageMap(false);
Object result = null;
if (map != null) {
result = map.get(key);
}
// warn if the storageMap was inherited from another thread and it is
// possibly problematic to use an object of this type in a background thread
if (result != null && logUsageOfProblematicInheritedValues() && !wasKeyAddedInCurrentThread(key)) {
for(Class<?> type: problematicTypes()) {
if(type.isAssignableFrom(result.getClass())) {
String msg = "The object for key '" + key + "' was inherited from the parent thread. " +
"The usage of inherited objects that are a subclass of '"+type.getSimpleName()+"' can cause problems.";
log.warn(msg, new Exception("DEBUG"));
}
}
if(problematicKeys().contains(key)) {
String msg = "The object for key '" + key + "' was inherited from the parent thread. " +
"The usage of inherited objects for this key can cause problems.";
log.warn(msg, new Exception("DEBUG"));
}
}
return result;
}
/**
* Gets the object associated with the key in the storage
* map off of the current thread in the given editing context.
* Throws a ClassCastException when the value is not an EO.
* @param ec editing context to retrieve the value into
* @param key key to be used to retrieve value from map.
* @return the value stored in the map for the given key.
*/
public static Object valueForKey(EOEditingContext ec, String key) {
Object result = valueForKey(key);
if(result != null) {
if (result instanceof EOEnterpriseObject) {
EOEnterpriseObject eo = (EOEnterpriseObject) result;
if(eo.editingContext() != null && eo.editingContext() != ec) {
eo.editingContext().lock();
try {
result = ERXEOControlUtilities.localInstanceOfObject(ec, eo);
} finally {
eo.editingContext().unlock();
}
}
} else {
throw new ClassCastException("Expected EO, got : " + result.getClass().getName() + ", " + result);
}
}
return result;
}
/**
* Gets the storage map from the current thread.
* At the moment this Map is syncronized for thread
* safety. This might not be necessary in which case
* users of this method would need to make sure that
* they take the appropriate precautions.
* @return Map object associated with this particular
* thread.
*/
public static Map map() {
return storageMap(true);
}
/**
* Removes all of the keys from the current Map.
*/
public static void reset() {
Map map = storageMap(false);
if (map != null)
map.clear();
}
/**
* Gets the {@link Map} from the thread map. Has the option to
* to create the map if it hasn't been created yet for this thread.
* Only used internally.
* @param create should create the map storage if it isn't found.
* @return the map for the current thread or null
*/
private static Map storageMap(boolean create) {
Map map = (Map)threadMap.get();
if (map == null && create) {
map = new HashMap(DefaultHashMapSize);
threadMap.set(map);
}
return map;
}
/**
* Registers that a key was added in the current thread.
* Only applies if the storageMap was inherited from the parent thread.
* @param key to bless
*/
private static void markKeyAddedInCurrentThread(String key) {
if (wasInheritedFromParentThread()) {
Map map = storageMap(false);
Set blessedKeys = (Set<String>) map.get(KEYS_ADDED_IN_CURRENT_THREAD_KEY);
if (blessedKeys == null) {
blessedKeys = new HashSet<String>();
map.put(KEYS_ADDED_IN_CURRENT_THREAD_KEY, blessedKeys);
}
blessedKeys.add(key);
}
}
/**
* Checks if a key was added in the current thread.
* Only applies if the storageMap was inherited from the parent thread.
* @param key to check
* @return boolean indicating if the key was added in the current thread
*/
private static boolean wasKeyAddedInCurrentThread(String key) {
if(!wasInheritedFromParentThread()) {
return true;
}
Map map = storageMap(false);
Set blessedKeys = (Set<String>) map.get(KEYS_ADDED_IN_CURRENT_THREAD_KEY);
return blessedKeys != null && blessedKeys.contains(key);
}
/**
* Checks if the storageMap was inherited from the parent thread.
* @return boolean indicating if the storageMap was inherited from another thread
*/
public static boolean wasInheritedFromParentThread() {
boolean result = false;
if (useInheritableThreadLocal()) {
Map map = storageMap(false);
if (map != null) {
result = ERXValueUtilities.booleanValue(map.get(ERXThreadStorage.WAS_CLONED_MARKER));
}
}
return result;
}
/**
* Set the Set of classes for which a warning is issued when the storageMap
* was inherited from another Thread and the object retrieved from the map
* is a subclass of one of the classes in the set.
*
* @param problematicTypes
* a set of classes to check
*/
public static void setProblematicTypes(NSSet<Class<?>> problematicTypes) {
_problematicTypes = problematicTypes == null ? NSSet.EmptySet : problematicTypes;
}
/**
* Retrieve the Set of classes for which a warning is issued when the
* storageMap was inherited from another Thread and the object retrieved
* from the map is a subclass of one of the classes in the set. Defaults to
* a Set containing WOSession.class, WOContext.class,
* EOEnterpriseObject.class and EOEditingContext.class
*
* @return the set of classes to check
*/
public static Set<Class<?>> problematicTypes() {
return _problematicTypes;
}
/**
* Set the Set of keys for which a warning is issued when the storageMap
* was inherited from another Thread and the key is accessed.
*
* @param problematicKeys
* a set of keys to check
*/
public static void setProblematicKeys(Set<String> problematicKeys) {
_problematicKeys = problematicKeys == null ? NSSet.EmptySet : problematicKeys;
}
/**
* Retrieve the Set of keys for which a warning is issued when the storageMap
* was inherited from another Thread and the key is accessed. Defaults to a
* set containing ERXWOContext.CONTEXT_DICTIONARY_KEY
* @return the set of keys to check
*/
public static Set<String> problematicKeys() {
return _problematicKeys;
}
protected static class ERXThreadStorageCloneableThreadLocal extends ERXCloneableThreadLocal {
@Override
protected Object childValue(Object parentValue) {
Map map = (Map) super.childValue(parentValue);
if (map != null) {
// set marker indicating that the map was inherited from the parent thread
map.put(ERXThreadStorage.WAS_CLONED_MARKER, Boolean.TRUE);
// reset set of blessed keys
map.remove(ERXThreadStorage.KEYS_ADDED_IN_CURRENT_THREAD_KEY);
}
return map;
}
}
}