package er.extensions.components; import java.io.Serializable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOElement; import com.webobjects.appserver.WOResponse; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import er.extensions.foundation.ERXExceptionUtilities; import er.extensions.foundation.ERXMutableDictionary; import er.extensions.foundation.ERXSimpleTemplateParser; /** * ERXInlineTemplate allows to specify a component's template dynamically. * <p> * The content which would usually go into the ".html" file within a WOComponent's bundle, is specified using the "html" * binding, the ".wod" part is specified by the "wod" binding. * <p> * When using {@link WOOgnl} with "ognl.helperFunctions = true" and "ognl.inlineBindings = true", you can leave out the * WOD part. * <p> * When keys are accessed, the component first determines the first element of the path (e.g. key "foo" for path * "foo.bar") and looks, if there is a binding with that key. * If there is such a binding, the value is retrieved and the rest of the keyPath applied to it * (valueForBinding("foo").valueForKeyPath("bar")). * If there is no binding with that name and "proxyParent" is true, the keyPath is resolved against the parent component. * Otherwise, dynamicBindings ({@link ERXComponent#dynamicBindings()}) are used. * You can switch off the usage of dynamicBindings by setting the binding "defaultToDynamicBindings" to false. * Then a warning will be logged for unknown keys. * <p> * When an error occurs, an error message is displayed. The message can be altered using the "errorTemplate" binding. * <p> * Optionally, a "cacheKey" (String) can be specified, under which the parsed WOElement will be cached. To allow * updating, a "cacheVersion" (Object) is available. When the version changes, the value is recalculated. * * @binding html HTML-part of the component (required) * @binding wod WOD-part of the component (optional) * @binding cacheKey Key under which to cache the WOElement (optional) * @binding cacheVersion Hint to determine if the cached object is up-to-date (optional) * @binding errorTemplate Template to use for displaying error messages. Uses {@link ERXSimpleTemplateParser} for display. * Method name and HTML-escaped message are provided by the "method" and "message" keys. (optional) * @binding proxyParent whether to proxy key path lookup to the parent (default is false) * @binding defaultToDynamicBindings whether to use dynamicBindings for unknown keys (default is true) * * @author th */ public class ERXInlineTemplate extends ERXNonSynchronizingComponent { /** * 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 static final Logger log = LoggerFactory.getLogger(ERXInlineTemplate.class); private static final String ERROR_TEMPLATE_DEFAULT = "<div class=\"ERXInlineTemplateError\" style=\"background-color: #faa; border: 2px dotted red;\">@@message@@</div>"; private static final String ERROR_TEMPLATE_BINDING = "errorTemplate"; private static final String CACHE_KEY_BINDING = "cacheKey"; private static final String CACHE_VERSION_BINDING = "cacheVersion"; private static final String TEMPLATE_HTML_BINDING = "html"; private static final String TEMPLATE_WOD_BINDING = "wod"; private static final String PROXY_PARENT_BINDING = "proxyParent"; private static final String DEFAULT_TO_DYNAMIC_BINDINGS_BINDING = "defaultToDynamicBindings"; private static NSMutableDictionary<String, CacheEntry> _cache = ERXMutableDictionary.synchronizedDictionary(); protected Error _deferredError = null; public ERXInlineTemplate(WOContext context) { super(context); } @Override public void appendToResponse(WOResponse woresponse, WOContext wocontext) { if (_deferredError != null) { woresponse.appendContentString(_deferredError.formatWithTemplate(errorTemplate())); } else { try { super.appendToResponse(woresponse, wocontext); } catch (Throwable t) { woresponse.appendContentString(new Error("appendToResponse", t).formatWithTemplate(errorTemplate())); } } _deferredError = null; } public String errorTemplate() { return stringValueForBinding(ERROR_TEMPLATE_BINDING, ERROR_TEMPLATE_DEFAULT); } public boolean proxyParent() { return booleanValueForBinding(PROXY_PARENT_BINDING); } public boolean defaultToDynamicBindings() { return booleanValueForBinding(DEFAULT_TO_DYNAMIC_BINDINGS_BINDING, true); } @Override public void takeValueForKeyPath(Object value, String keyPath) { try { NSMutableArray<String> keyPathComponents = NSArray.componentsSeparatedByString(keyPath, ".").mutableClone(); String firstKey = keyPathComponents.removeObjectAtIndex(0); if (bindingKeys().contains(firstKey)) { if (keyPathComponents.count() > 0) { Object o = valueForBinding(firstKey); String remainingKeyPath = keyPathComponents.componentsJoinedByString("."); log.debug("set binding using keypath {} / {}", firstKey, remainingKeyPath); NSKeyValueCodingAdditions.Utility.takeValueForKeyPath(o, value, remainingKeyPath); } else { log.debug("set binding value {}", firstKey); setValueForBinding(value, firstKey); } } else if (proxyParent()) { log.debug("set parent binding {}", keyPath); parent().takeValueForKeyPath(value, keyPath); } else if (defaultToDynamicBindings()){ log.debug("set dynamic binding {}", keyPath); dynamicBindings().takeValueForKeyPath(value, keyPath); } else { log.warn("Unknown keyPath: {}", keyPath); } } catch (Throwable t) { _deferredError = new Error("takeValueForKeyPath", t); } } @Override public Object valueForKeyPath(String keyPath) { try { NSMutableArray<String> keyPathComponents = NSArray.componentsSeparatedByString(keyPath, ".").mutableClone(); String firstKey = keyPathComponents.removeObjectAtIndex(0); Object value = null; if (bindingKeys().contains(firstKey)) { Object o = valueForBinding(firstKey); if (keyPathComponents.count() > 0) { String remainingKeyPath = keyPathComponents.componentsJoinedByString("."); log.debug("get binding using keypath {} / {}", firstKey, remainingKeyPath); value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(o, remainingKeyPath); } else { log.debug("get binding value {}", firstKey); value = o; } } else if (proxyParent()) { log.debug("get parent binding {}", keyPath); value = parent().valueForKeyPath(keyPath); } else if (defaultToDynamicBindings()) { log.debug("get dynamic binding {}", keyPath); value = dynamicBindings().valueForKeyPath(keyPath); } else { log.warn("Unknown keyPath: {}", keyPath); } return value; } catch (Throwable t) { // save throwable _deferredError = new Error("takeValueForKeyPath", t); return null; } } @Override public void takeValueForKey(Object obj, String s) { takeValueForKeyPath(obj, s); } @Override public Object valueForKey(String s) { return valueForKeyPath(s); } @Override public WOElement template() { try { WOElement element = null; String cacheKey = (String) valueForBinding(CACHE_KEY_BINDING); if (cacheKey != null) { // should cache CacheEntry cacheEntry = _cache.objectForKey(cacheKey); Object requestedVersion = valueForBinding(CACHE_VERSION_BINDING); if (cacheEntry != null && (requestedVersion == null || requestedVersion.equals(cacheEntry.version()))) { // requestedVersion matches or is null log.debug("using cache: {} / {}", cacheKey, cacheEntry.version()); element = cacheEntry.element(); } else { // no matching cache entry log.debug("updating cache: {} / {} -> {}", cacheKey, (cacheEntry == null ? null : cacheEntry.version()), requestedVersion); element = _template(); cacheEntry = new CacheEntry(requestedVersion, element); _cache.takeValueForKey(cacheEntry, cacheKey); } } else { // no caching log.debug("caching disabled"); element = _template(); } return element; } catch (Throwable t) { String html = new Error("template", t).formatWithTemplate(errorTemplate()); return WOComponent.templateWithHTMLString("", "", html, "", null, WOApplication.application().associationFactoryRegistry(), WOApplication.application().namespaceProvider()); } } private WOElement _template() { String html = stringValueForBinding(TEMPLATE_HTML_BINDING, ""); String wod = stringValueForBinding(TEMPLATE_WOD_BINDING, ""); WOElement element = WOComponent.templateWithHTMLString("", "", html, wod, null, WOApplication.application().associationFactoryRegistry(), WOApplication.application().namespaceProvider()); return element; } class CacheEntry { private WOElement _element; private Object _version; public CacheEntry(Object version, WOElement element) { _version = version; _element = element; } public WOElement element() { return _element; } public Object version() { return _version; } } public static class Error 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 Throwable _t; private String _method; public Error(String method, Throwable t) { log.error("{}: {}", method, t, t); _t = t; _method = method; } public String message() { String s = ERXExceptionUtilities.toParagraph(_t); if (s != null) { s = s.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\n", "<br />"); } return s; } public String method() { return _method; } public String formatWithTemplate(String template) { return ERXSimpleTemplateParser.sharedInstance().parseTemplateWithObject(template, ERXSimpleTemplateParser.DEFAULT_DELIMITER, this); } } }