package er.extensions.components;
import java.util.Objects;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WODirectAction;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver.WOResponse;
import com.webobjects.appserver.WOSession;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import er.extensions.appserver.ERXApplication;
import er.extensions.appserver.ERXResourceManager;
import er.extensions.appserver.ERXResponse;
import er.extensions.appserver.ERXResponseRewriter;
import er.extensions.appserver.ajax.ERXAjaxApplication;
import er.extensions.foundation.ERXExpiringCache;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
/**
* Adds a style sheet to a page. You can either supply a complete URL, a file
* and framework name or put something in the component content. The content of
* the component is cached under a "key" binding and then delivered via a direct
* action, so it doesn't need to get re-rendered too often.
*
* @binding filename name of the style sheet
* @binding framework name of the framework for the style sheet
* @binding href url to the style sheet
* @binding key key to cache the style sheet under when using the component
* content. Default is the sessionID. That means, you should *really*
* explicitly set a key, when you use more than one ERXStyleSheet using
* the component content method within one session
* @binding inline when <code>true</code>, the generated link tag will be appended inline,
* when <code>false</code> it'll be placed in the head of the page, when unset it
* will be placed inline for ajax requests and in the head for regular
* requests
* @binding media media name this style sheet is for
* @property er.extensions.ERXStyleSheet.xhtml (defaults <code>true</code>) if <code>false</code>,
* link tags are not closed, which is compatible with older HTML
*/
// FIXME: cache should be able to cache on values of bindings, not a single key
public class ERXStyleSheet 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;
/**
* Public constructor
*
* @param aContext
* a context
*/
public ERXStyleSheet( WOContext aContext ) {
super( aContext );
}
protected static ERXExpiringCache<String, WOResponse> cache( WOSession session ) {
ERXExpiringCache<String, WOResponse> cache = (ERXExpiringCache<String, WOResponse>)session.objectForKey( "ERXStylesheet.cache" );
if( cache == null ) {
cache = new ERXExpiringCache<>( 60 );
cache.startBackgroundExpiration();
session.setObjectForKey( cache, "ERXStylesheet.cache" );
}
return cache;
}
public static class Sheet extends WODirectAction {
public Sheet( WORequest worequest ) {
super( worequest );
}
@Override
public WOActionResults performActionNamed( String name ) {
WOResponse response = ERXStyleSheet.cache( session() ).objectForKey( name );
String md5 = ERXStringUtilities.md5Hex( response.contentString(), null );
String queryMd5 = response.headerForKey( "checksum" );
if (Objects.equals(md5, queryMd5)) {
//TODO check for last-whatever time and return not modified if not changed
}
return response;
}
}
/**
* Returns the complete url to the style sheet.
*
* @return style sheet url
*/
public String styleSheetUrl() {
String url = stringValueForBinding("styleSheetUrl");
url = (url == null ? stringValueForBinding("href") : url);
if( url == null ) {
String name = styleSheetName();
if( name != null ) {
url = application().resourceManager().urlForResourceNamed( name, styleSheetFrameworkName(), languages(), context().request() );
if( ERXResourceManager._shouldGenerateCompleteResourceURL( context() ) ) {
url = ERXResourceManager._completeURLForResource( url, null, context() );
}
}
}
return url;
}
/**
* Returns the style sheet framework name either resolved via the binding
* <b>framework</b>.
*
* @return style sheet framework name
*/
public String styleSheetFrameworkName() {
String result = stringValueForBinding("styleSheetFrameworkName");
result = (result == null ? stringValueForBinding("framework") : result);
return result;
}
/**
* Returns the style sheet name either resolved via the binding <b>filename</b>.
*
* @return style sheet name
*/
public String styleSheetName() {
String result = stringValueForBinding("styleSheetName");
result = (result == null ? stringValueForBinding("filename") : result);
return result;
}
/**
* Returns key under which the stylesheet should be placed in the cache. If
* no key is given, the session id is used.
*
* @return cache key
*/
public String styleSheetKey() {
String result = stringValueForBinding("key");
if( result == null ) {
result = session().sessionID();
}
return result;
}
/**
* Specifies on what device the linked document will be displayed.
* @return media string
*/
public String mediaType() {
return stringValueForBinding( "media" );
}
/**
* Returns the languages for the request.
* @return requested languages
*/
private NSArray<String> languages() {
if( hasSession() ) {
return session().languages();
}
WORequest request = context().request();
if( request != null ) {
return request.browserLanguages();
}
return null;
}
/**
* Appends the <link> tag, either by using the style sheet name and
* framework or by using the component content and then generating a link to
* it.
*/
@Override
public void appendToResponse( WOResponse originalResponse, WOContext wocontext ) {
String styleSheetFrameworkName = styleSheetFrameworkName();
String styleSheetName = styleSheetName();
boolean isResourceStyleSheet = styleSheetName != null;
if( isResourceStyleSheet && ERXResponseRewriter.isResourceAddedToHead( wocontext, styleSheetFrameworkName, styleSheetName ) ) {
// Skip, because this has already been added ...
return;
}
// default to inline for ajax requests
boolean inline = booleanValueForBinding( "inline", ERXAjaxApplication.isAjaxRequest( wocontext.request() ) );
WOResponse response = inline ? originalResponse : new ERXResponse();
String href = styleSheetUrl();
if( href == null ) {
String key = styleSheetKey();
ERXExpiringCache<String, WOResponse> cache = cache( session() );
String md5;
WOResponse cachedResponse = cache.objectForKey( key );
if( cache.isStale( key ) || ERXApplication.isDevelopmentModeSafe() ) {
cachedResponse = new ERXResponse();
super.appendToResponse( cachedResponse, wocontext );
// appendToResponse above will change the response of
// "wocontext" to "newresponse". When this happens during an
// Ajax request, it will lead to backtracking errors on
// subsequent requests, so restore the original response "r"
wocontext._setResponse( originalResponse );
cachedResponse.setHeader( "text/css", "content-type" );
cache.setObjectForKey( cachedResponse, key );
md5 = ERXStringUtilities.md5Hex( cachedResponse.contentString(), null );
cachedResponse.setHeader( md5, "checksum" );
}
md5 = cachedResponse.headerForKey( "checksum" );
NSDictionary<String, Object> query = new NSDictionary<>( md5, "checksum" );
href = wocontext.directActionURLForActionNamed( Sheet.class.getName() + "/" + key, query, wocontext.request().isSecure(), 0, false );
}
response._appendContentAsciiString( "<link" );
if (styleSheetName != null && styleSheetName.toLowerCase().endsWith(".less")) {
response._appendTagAttributeAndValue( "rel", "stylesheet/less", false );
} else {
response._appendTagAttributeAndValue( "rel", "stylesheet", false );
}
response._appendTagAttributeAndValue( "type", "text/css", false );
response._appendTagAttributeAndValue( "href", href, false );
response._appendTagAttributeAndValue( "media", mediaType(), false );
if( ERXStyleSheet.shouldCloseLinkTags() ) {
response._appendContentAsciiString( "/>" );
} else {
response._appendContentAsciiString( ">" );
}
response.appendContentString("\n");
boolean inserted = true;
if( !inline ) {
String stylesheetLink = response.contentString();
inserted = ERXResponseRewriter.insertInResponseBeforeHead( originalResponse, wocontext, stylesheetLink, ERXResponseRewriter.TagMissingBehavior.Inline );
}
if( inserted ) {
if( isResourceStyleSheet ) {
ERXResponseRewriter.resourceAddedToHead( wocontext, styleSheetFrameworkName, styleSheetName );
}
else if( href != null ) {
ERXResponseRewriter.resourceAddedToHead( wocontext, null, href );
}
}
}
/**
* Returns whether or not XHTML link tags should be used. If false, then
* link tags will not be closed, which is more compatible with certain
* browser parsers. Set the 'er.extensions.ERXStyleSheet.xhtml' to control
* this property.
*
* @return true of link tags should be closed, false otherwise
*/
public static boolean shouldCloseLinkTags() {
return ERXProperties.booleanForKeyWithDefault( "er.extensions.ERXStyleSheet.xhtml", true );
}
}