package er.extensions.components;
import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver.WOResponse;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSPropertyListSerialization;
import er.extensions.foundation.ERXAssert;
/**
* Wrapper that caches its content based on a set of bindings. Use this component to wrap
* parts of your HTML whose generation is costly.
* <p>
* Valid keys would be for example:
* <ul>
* <li><code>parent.isEnabled</code>, where isEnabled would be some method on the parent.
* <li><code>session.user.name</code>
* <li><code>headers.hostName</code>
* <li><code>formValues.oid</code>
* <li><code>session.localizer.language</code>
* </ul>
* Basically, you would put there any key whose value would cause the content to change.
* Session IDs are replaced automatically. Don't use this wrapper if the content contains
* component actions. Drop only stateless components in this wrapper.
*
* @binding keys the keys to use for caching
* @binding duration the duration the entry stays in the cache
* @binding entryName the name to cache on
*
* @author ak on 20.01.05
*/
//ENHANCEME cache should get reaped every so often and remove stale entries.
public class ERXCachingWrapper extends ERXStatelessComponent {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
/** The cached entries */
protected static Map cache = Collections.synchronizedMap(new HashMap() {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
@Override
public Object get(Object key) {
CacheEntry result = (CacheEntry) super.get(key);
if(result != null) {
if(!result.isActive()) {
remove(key);
result = null;
}
}
return result;
}
});
/** Simply cache entry class. It caches a string for a duration and can replace the session ID on retrieval. */
protected static class CacheEntry implements Serializable{
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
private long insertTime;
private long duration;
private String content;
private String sessionID;
public CacheEntry(String aContent, long aDuration, String aSessionID) {
insertTime = System.currentTimeMillis();
content = aContent;
duration = aDuration;
sessionID = aSessionID;
}
public boolean isActive() {
return System.currentTimeMillis() - (insertTime + duration) < 0;
}
public String content(WOContext arg1) {
if(sessionID != null) {
return content.replaceAll(sessionID, arg1.session().sessionID());
}
return content;
}
}
protected NSArray keys;
protected String entryName;
protected Long cacheDuration;
protected CacheEntry entry;
protected NSDictionary values;
/**
* Public constructor
* @param context the context
*/
public ERXCachingWrapper(WOContext context) {
super(context);
}
@Override
public void awake() {
super.awake();
keys = null;
entryName = null;
cacheDuration = null;
values = null;
entry = (CacheEntry) cache.get(values());
}
protected NSArray keys() {
if(keys == null) {
Object value = valueForBinding("keys");
if(value instanceof NSArray) {
keys = (NSArray)value;
} else if(value instanceof String) {
keys = (NSArray) NSPropertyListSerialization.propertyListFromString((String) value);
} else if (value != null) {
throw new IllegalArgumentException("keys must be a NSArray or a property list String");
}
if(keys == null) {
keys = NSArray.EmptyArray;
}
}
return keys;
}
/**
* Returns the request headers as a KVC object.
*/
// ENHANCEME use symbolic names like "remoteHost" to broker between all those different adaptor keys
public NSKeyValueCoding headers() {
return new NSKeyValueCoding() {
public Object valueForKey(String s) {
return context().request().headerForKey(s);
}
public void takeValueForKey(Object obj, String s) {
}
};
}
/**
* Returns the form values as a KVC object.
*/
public NSKeyValueCoding formValues() {
return new NSKeyValueCoding() {
public Object valueForKey(String s) {
return context().request().formValueForKey(s);
}
public void takeValueForKey(Object obj, String s) {
}
};
}
protected String entryName() {
if(entryName == null) {
entryName = (String)valueForBinding("entryName");
}
ERXAssert.PRE.notNull("cacheEntryName is required", entryName);
return entryName;
}
protected long cacheDuration() {
if(cacheDuration == null) {
Number value = (Number)valueForBinding("duration");
if(value == null) {
cacheDuration = Long.valueOf(60L*1000L);
} else {
cacheDuration = Long.valueOf(value.longValue());
}
}
return cacheDuration.longValue();
}
protected NSDictionary values() {
if(values == null) {
NSMutableDictionary result = new NSMutableDictionary();
for(Enumeration e = keys().objectEnumerator(); e.hasMoreElements();) {
String keyPath = (String)e.nextElement();
Object value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(this, keyPath);
if(value != null) {
result.setObjectForKey(value, keyPath);
}
}
result.setObjectForKey(entryName(), "ERXCachingWrapper.entryName");
values = result.immutableClone();
}
return values;
}
@Override
public void takeValuesFromRequest(WORequest request, WOContext context) {
if(entry == null) {
super.takeValuesFromRequest(request, context);
}
}
@Override
public WOActionResults invokeAction(WORequest request, WOContext context) {
if(entry == null) {
return super.invokeAction(request, context);
}
return null;
}
@Override
public void appendToResponse(WOResponse response, WOContext context) {
if(entry == null) {
WOResponse newResponse = application().createResponseInContext(context);
newResponse.setHeaders(response.headers());
newResponse.setUserInfo(response.userInfo());
super.appendToResponse(newResponse, context);
String content = newResponse.contentString();
entry = new CacheEntry(content, cacheDuration(), (context.hasSession() ? context.session().sessionID() : null));
cache.put(values(), entry);
}
String content = entry.content(context);
response.appendContentString(content);
}
}