package er.extensions.appserver; import java.util.LinkedHashMap; import java.util.Map; import java.util.Stack; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOMessage; import com.webobjects.appserver.WOResponse; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableData; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSRange; import er.extensions.foundation.ERXThreadStorage; /** * ERXResponse provides a place to override methods of WOResponse. This is * returned by default from ERXApplication. Also has support for "partials", * i.e. in your render tree, you can define a new "partial", where the content * will actually get rendered. * * @author mschrag * @author ak */ public class ERXResponse extends WOResponse { public static final String ContentDispositionHeaderKey = "content-disposition"; public static final String ContentTypeHeaderKey = "content-type"; public static final String DisablePageCachingKey = "com.webobjects.appserver.Response.DisablePageCaching"; public static class Context { protected LinkedHashMap<String, ERXResponse> partials = new LinkedHashMap<>(); protected Stack<ERXResponse> stack = new Stack<>(); } private LinkedHashMap<String, Integer> marks; private Stack<Object> _contentStack; private WOContext _context; private boolean _allowClientCaching; public ERXResponse() { _allowClientCaching = false; } /** * Convenience constructor for direct actions. * @param content text content of the response * @param status HTTP status code of the response */ public ERXResponse(String content, int status) { this(content); setStatus(status); } /** * Convenience constructor for direct actions. * @param content text content of the response */ public ERXResponse(String content) { setContent(content); } /** * Convenience constructor for direct actions. * @param status HTTP status code of the response */ public ERXResponse(int status) { setStatus(status); } public ERXResponse(WOContext context) { _context = context; } protected void __setContent(Object appendable) { try { WOMessage.class.getDeclaredField("_content").set(this, appendable); } catch (Throwable e) { throw new NSForwardException(e); } } /** * Pushes a new _content onto the stack, so you can write to this response * and capture the output. */ public void pushContent() { if (_contentStack == null) { _contentStack = new Stack<>(); } _contentStack.push(_content); Object newContent; try { newContent = _content.getClass().newInstance(); } catch (Throwable e) { throw new NSForwardException(e); } __setContent(newContent); } /** * Pops the last _content off the stack, optionally appending the current * content to it. * * @param append */ public void popContent(boolean append) { if (_contentStack == null || _contentStack.size() == 0) { throw new IllegalStateException("You attempted to popContent off of an empty stack."); } Object oldAppendable = _content; Object appendable = _contentStack.pop(); __setContent(appendable); if (append) { appendContentString(oldAppendable.toString()); } } /** * Call this to mark the place where a partial should get rendered. * * @param key */ public void mark(String key) { if (marks == null) { marks = new LinkedHashMap<>(); } marks.put(key, _contentLength()); } /** * Overridden to insert the partials in the respective area. * @param originalContext context */ @Override public void _finalizeInContext(WOContext originalContext) { super._finalizeInContext(originalContext); if (marks != null && marks.size() > 0) { Context context = currentContext(); NSMutableData content = new NSMutableData(); int last = 0; for (Map.Entry<String, Integer> entry : marks.entrySet()) { String key = entry.getKey(); Integer offset = entry.getValue(); NSRange range = new NSRange(last, offset - last); NSData data = content().subdataWithRange(range); content.appendData(data); ERXResponse partial = context.partials.get(key); if (partial != null) { NSData partialData = partial.content(); content.appendData(partialData); } last = offset; } NSRange range = new NSRange(last, _contentLength() - last); NSData data = content().subdataWithRange(range); content.appendData(data); setContent(content); } } private static Context currentContext() { Context context = (Context) ERXThreadStorage.valueForKey("ERXResponse.Context"); if (context == null) { context = new Context(); ERXThreadStorage.takeValueForKey(context, "ERXResponse.Context"); } return context; } /** * Returns the associated response for the supplied key. Creates it if * needed. * * @param key * the key to push the partial as * @return the new ERXResponse to write to */ public static ERXResponse pushPartial(String key) { Context context = currentContext(); WOContext wocontext = ERXWOContext.currentContext(); context.stack.push((ERXResponse) wocontext.response()); ERXResponse response = context.partials.get(key); if (response == null) { response = new ERXResponse(wocontext); context.partials.put(key, response); } wocontext._setResponse(response); return response; } /** * Returns the top-most response after this one has been pulled from the * stack. * * @return the previous partial */ public static ERXResponse popPartial() { Context context = currentContext(); ERXResponse response = context.stack.pop(); WOContext wocontext = ERXWOContext.currentContext(); wocontext._setResponse(response); return response; } /** * Overridden to <b>not</b> call super if trying to download an attachment * to IE or if allowClientCaching is <code>true</code>. * * @see com.webobjects.appserver.WOResponse#disableClientCaching() * */ @Override public void disableClientCaching() { boolean isIEDownloadingAttachment = isIE() && isAttachment() && !isHTML(); if (!isIEDownloadingAttachment && !_allowClientCaching) { //NSLog.out.appendln("Disabling client caching"); super.disableClientCaching(); } else { //NSLog.out.appendln("Allowing IE client caching"); } } /** * Can be used to enable client-side caching of the response content, * for example if the response contains a static image. * Normally it will be prevented by {@link #disableClientCaching()}. * Note that additionally you might need to set the cache-control header. * * @param allowClientCaching <code>true</code> prevents calling {@link #disableClientCaching()} */ public void setAllowClientCaching(boolean allowClientCaching) { this._allowClientCaching = allowClientCaching; } /** * @return <code>true</code> if client-side caching shall be allowed */ public boolean allowClientCaching() { return _allowClientCaching; } /** * @see #DisablePageCachingKey * @return <code>true</code> if disablePageCaching() has been called for * this response */ public boolean isPageCachingDisabled() { return userInfoForKey(DisablePageCachingKey) != null; } /** * WO 5.4 API Sets the value for key in the user info dictionary. * * @param value * value to add to userInfo() * @param key * key to add value under */ @Override public void setUserInfoForKey(Object value, String key) { /** * require [valid_value] value != null; [valid_key] key != null; **/ NSMutableDictionary newUserInfo = new NSMutableDictionary(value, key); if (userInfo() != null) { newUserInfo.addEntriesFromDictionary(userInfo()); } setUserInfo(newUserInfo); /** ensure [value_set] userInfoForKey(key).equals(value); **/ } /** * WO 5.4 API * * @param key * key to return value from userInfo() for * @return value from {@link #userInfo()} for key, or <code>null</code> if not available */ @Override public Object userInfoForKey(String key) { /** require [valid_key] key != null; **/ return userInfo() != null ? userInfo().objectForKey(key) : null; } public boolean isAttachment() { String contentDisposition = contentDisposition(); return contentDisposition != null && (contentDisposition.indexOf("inline") > -1 || contentDisposition.indexOf("attachment") > -1); } /** * @return <code>true</code> if the content type of this response indicates * HTML */ public boolean isHTML() { return contentType() != null && contentType().toLowerCase().indexOf("text/html") > -1; } /** * @return header value for ContentDispositionHeaderKey */ public String contentDisposition() { return headerForKey(ContentDispositionHeaderKey); } /** * @return header value for ContentTypeHeaderKey */ public String contentType() { return headerForKey(ContentTypeHeaderKey); } /** * @return <code>true</code> if the Request this Response is for has a user * agent that indicates and IE browser */ public boolean isIE() { boolean isIE = false; if (_context != null && _context.request() instanceof ERXRequest) { isIE = ((ERXRequest) _context.request()).browser().isIE(); } return isIE; } /** * Sets whether the given response should turn on XHTML tag rendering. * * @param response the response to turn XHTML on for * @param xhtml whether or not XHTML should be turned on */ public static void setXHTML(WOResponse response, boolean xhtml) { response.setHeader(String.valueOf(xhtml), "x-wo-xml-tags"); } /** * Returns whether or not XHTML is turned on for the given response. * * @param response the response to check XHTML for * @return whether or not XHTML was turned on */ public static boolean isXHTML(WOResponse response) { return "true".equals(response.headerForKey("x-wo-xml-tags")); } }