package er.extensions.components; import java.util.Stack; import org.apache.log4j.Logger; import com.webobjects.appserver.WOAssociation; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOElement; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver._private.WODynamicGroup; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import er.extensions.appserver.ERXWOContext; /** * ERXDynamicElement provides a common base class for dynamic elements. * <p> * All subclasses of ERXDynamicElement have to be thread safe! The WOAssociation objects * are thread safe as they don't hold a specific value but are used to retrieve the correct * value from the current parent component. If you want or have to store values in instance * variables use the inner class {@link ContextData} as value holder. Those objects will * store values indirectly within the current context which means you can declare them as * static. * * @author jw */ public abstract class ERXDynamicElement extends WODynamicGroup { protected Logger log = Logger.getLogger(getClass()); private final NSDictionary<String, WOAssociation> _associations; public ERXDynamicElement(String name, NSDictionary<String, WOAssociation> associations, WOElement template) { super(name, associations, template); _associations = associations; } public ERXDynamicElement(String name, NSDictionary<String, WOAssociation> associations, NSMutableArray<WOElement> children) { super(name, associations, children); _associations = associations; } /** * Returns the element's association dictionary. * * @return the element's association dictionary */ public NSDictionary<String, WOAssociation> associations() { return _associations; } /** * Return the value of the id binding if it exists or a safe identifier * otherwise. * * @param context context of the transaction * @return id string for this component */ public String id(WOContext context) { String id = stringValueForBinding("id", context.component()); if (id == null) { id = ERXWOContext.safeIdentifierName(context, false); } return id; } /** * Returns the name of this element within the given context. This * corresponds to the elementID. * * @param context context of the transaction * @return elementID */ protected String nameInContext(WOContext context) { return context.elementID(); } /** * Checks if we are in secure mode by checking the secure binding or the * context's secure mode as fallback. * * @param context context of the transaction * @return <code>true</code> if in secure mode */ public boolean secureInContext(WOContext context) { if (hasBinding("secure")) { return booleanValueForBinding("secure", false, context.component()); } return context.secureMode(); } /** * Checks if there is an association for a binding with the given name. * * @param name binding name * @return <code>true</code> if the association exists */ public boolean hasBinding(String name) { return ERXComponentUtilities.hasBinding(name, associations()); } /** * Returns the association for a binding with the given name. If there is * no such association <code>null</code> will be returned. * * @param name binding name * @return association for given binding or <code>null</code> */ public WOAssociation bindingNamed(String name) { return ERXComponentUtilities.bindingNamed(name, associations()); } /** * Checks if the association for a binding with the given name can assign * values at runtime. * * @param name binding name * @return <code>true</code> if binding is settable */ public boolean bindingIsSettable(String name) { return ERXComponentUtilities.bindingIsSettable(name, associations()); } /** * Will try to set the given binding in the component to the passed value. * * @param value new value for the binding * @param name binding name * @param component component to set the value in */ public void setValueForBinding(Object value, String name, WOComponent component) { ERXComponentUtilities.setValueForBinding(value, name, associations(), component); } /** * Retrieves the current value of the given binding from the component. If there * is no such binding or its value evaluates to <code>null</code> the default * value will be returned. * * @param name binding name * @param defaultValue default value * @param component component to get value from * @return retrieved value or default value */ public Object valueForBinding(String name, Object defaultValue, WOComponent component) { return ERXComponentUtilities.valueForBinding(name, defaultValue, associations(), component); } /** * Retrieves the current value of the given binding from the component. If there * is no such binding <code>null</code> will be returned. * * @param name binding name * @param component component to get value from * @return retrieved value or <code>null</code> */ public Object valueForBinding(String name, WOComponent component) { return ERXComponentUtilities.valueForBinding(name, associations(), component); } /** * Retrieves the current string value of the given binding from the component. If there * is no such binding or its value evaluates to <code>null</code> the default * value will be returned. * * @param name binding name * @param defaultValue default value * @param component component to get value from * @return retrieved string value or default value */ public String stringValueForBinding(String name, String defaultValue, WOComponent component) { return ERXComponentUtilities.stringValueForBinding(name, defaultValue, associations(), component); } /** * Retrieves the current string value of the given binding from the component. If there * is no such binding <code>null</code> will be returned. * * @param name binding name * @param component component to get value from * @return retrieved string value or <code>null</code> */ public String stringValueForBinding(String name, WOComponent component) { return ERXComponentUtilities.stringValueForBinding(name, associations(), component); } /** * Retrieves the current boolean value of the given binding from the component. If there * is no such binding the default value will be returned. * * @param name binding name * @param defaultValue default value * @param component component to get value from * @return retrieved boolean value or default value */ public boolean booleanValueForBinding(String name, boolean defaultValue, WOComponent component) { return ERXComponentUtilities.booleanValueForBinding(name, defaultValue, associations(), component); } /** * Retrieves the current boolean value of the given binding from the component. If there * is no such binding <code>false</code> will be returned. * * @param name binding name * @param component component to get value from * @return retrieved boolean value or <code>false</code> */ public boolean booleanValueForBinding(String name, WOComponent component) { return ERXComponentUtilities.booleanValueForBinding(name, associations(), component); } /** * Retrieves the current int value of the given binding from the component. If there * is no such binding the default value will be returned. * * @param name binding name * @param defaultValue default value * @param component component to get value from * @return retrieved int value or default value */ public int integerValueForBinding(String name, int defaultValue, WOComponent component) { return ERXComponentUtilities.integerValueForBinding(name, defaultValue, associations(), component); } /** * Resolves a given binding as an NSArray object. * * @param <T> the type of the array's items * @param name binding name * @param component component to get value from * @return retrieved array value or <code>null</code> */ public <T> NSArray<T> arrayValueForBinding(String name, WOComponent component) { return ERXComponentUtilities.arrayValueForBinding(name, associations(), component); } /** * Resolves a given binding as an NSArray object. * * @param <T> the type of the array's items * @param name binding name * @param defaultValue default value * @param component component to get value from * @return retrieved array value or default value */ public <T> NSArray<T> arrayValueForBinding(String name, NSArray<T> defaultValue, WOComponent component) { return ERXComponentUtilities.arrayValueForBinding(name, defaultValue, associations(), component); } /** * Appends the attribute to the response. If the value is <code>null</code> * the appending is skipped. * * @param response the current response * @param name the attribute name * @param value the attribute value */ protected void appendTagAttributeToResponse(WOResponse response, String name, Object value) { if (value != null) { response._appendTagAttributeAndValue(name, value.toString(), true); } } /** * Convenience method to override if you want to store values in ContextData object. Here you can call * the {@link ContextData#begin(WOContext, Object)} on your ContextData objects. This method should then * be called at the beginning of the {@link #appendToResponse(WOResponse, WOContext)}, * {@link #takeValuesFromRequest(WORequest, WOContext)} and {@link #invokeAction(WORequest, WOContext)}. * * @param context context of the transaction */ protected void beforeProcessing(WOContext context) { } /** * Convenience method to override if you want to store values in ContextData object. Here you can call * the {@link ContextData#end(WOContext)} on your ContextData objects. This method should then * be called at the end of the {@link #appendToResponse(WOResponse, WOContext)}, * {@link #takeValuesFromRequest(WORequest, WOContext)} and {@link #invokeAction(WORequest, WOContext)}. * * @param context context of the transaction */ protected void afterProcessing(WOContext context) { } /** * This class is used to store values that cannot be stored in instance variables as * dynamic elements have to be thread-safe. Internally the userInfo of the current context is used * to store the values, so all information is only valid for one request-response cycle. A stack * is used per key so multiple nested components of the same type can use the same key safely. * <p> * You have to use <code>begin</code> before first usage of the field and <code>end</code> at * the end of the cycle to clean up the corresponding stack. Without balanced calls of * <code>begin</code> and <code>end</code> very bad things will happen. Thus it is advisable * to use a try-finally (see example below). As you have to make these calls during <code>appendToResponse</code>, * <code>taleValuesFromRequest</code> and <code>invokeAction</code> you should put those calls into * overridden methods of <code>beforeProcessing</code> and <code>afterProcessing</code> respectively * to avoid code duplication. * <p> * It is advisable too to choose a unique <code>key</code>, as different component types * will use the same userInfo. It is suggested to use a key composed by the component * and field name (e.g. <i>MyDynamicElement.myField</i>). * The ContextData field itself can and should be declared static as it doesn't store the * value itself, it is only used as an accessor to the value stored within the context. * <h3>Example:</h3> * <pre><code> * public static final ContextData<String> myField = new ContextData<>("MyClass.myField"); * * public void appendToResponse(WOResponse response, WOContext context) { * beforeProcessing(context); * try { * response.appendContentString("Current value: " + myField.value(context)); * } finally { * afterProcessing(context); * } * } * * public void takeValuesFromRequest(WORequest request, WOContext context) { * beforeProcessing(context); * try { * … * } finally { * afterProcessing(context); * } * } * * public WOActionResults invokeAction(WORequest request, WOContext context) { * beforeProcessing(context); * try { * … * } finally { * afterProcessing(context); * } * } * * protected void beforeProcessing(WOContext context) { * myField.begin(context, "myValue"); * } * * protected void afterProcessing(WOContext context) { * myField.end(context); * } * </code></pre> * * @param <T> type of data to store * @author sgaertner */ protected static class ContextData<T> { private final String _key; public ContextData(String key) { _key = key; } /** * The key this object uses to store values in the context's * userInfo. * * @return the key */ public String key() { return _key; } protected Stack<T> stack(WOContext context) { Stack<T> stack = (Stack<T>) context.userInfoForKey(_key); if (stack == null) { stack = new Stack<>(); context.setUserInfoForKey(stack, _key); } return stack; } /** * Push a constant value onto the stack. * * @param context context of the transaction * @param value constant value */ public void begin(WOContext context, T value) { stack(context).push(value); } /** * Push a value from a specific binding onto the stack. * * @param context context of the transaction * @param component the current dynamic element * @param name binding name */ public void begin(WOContext context, ERXDynamicElement component, String name) { stack(context).push((T) component.valueForBinding(name, context.component())); } /** * Replace the current value on the stack with a new value. * * @param context context of the transaction * @param value constant value */ public void setValue(WOContext context, T value) { stack(context).pop(); stack(context).push(value); } /** * Returns the current value. * * @param context context of the transaction * @return current value */ public T value(WOContext context) { return stack(context).peek(); } /** * Removes the current value from the stack. If you pushed multiple values onto * the stack by calling begin() several times you have to call end() the same * number of times. * * @param context context of the transaction */ public void end(WOContext context) { stack(context).pop(); } @Override public String toString() { return new StringBuilder().append('<').append(getClass().getName()).append(" key: ").append(_key).append('>') .toString(); } } }