package er.modern.directtoweb.components;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOContext;
import com.webobjects.directtoweb.D2WContext;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSNotification;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation.NSSelector;
import er.ajax.AjaxUpdateContainer;
import er.directtoweb.components.ERDCustomComponent;
import er.extensions.appserver.ERXWOContext;
import er.extensions.eof.ERXConstant;
import er.extensions.eof.ERXKey;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.foundation.ERXStringUtilities;
import er.modern.directtoweb.components.buttons.ERMDDeleteButton;
/**
* ERMDAjaxNotificationCenter makes it easy to observe properties for changes
* and update dependent property keys. You just specify a dependency structure
* via the propertyDependencies D2W key. It takes a dictionary with property
* keys to be observed as keys and an array of the dependents to be updated as
* the value. Example:
*
* <pre>
* 100 : ((task = 'create' or task = 'edit') and entity.name = 'Person') => propertyDependencies = {"isFemale" = ("salutation"); "dateOfBirth" = ("discount", "parentEmail"); } [com.webobjects.directtoweb.Assignment]
* </pre>
*
* This will observe the property keys "isFemale" and "dateOfBirth". If
* "isFemale" changes, the "salutation" property will be updated. If
* "dateOfBirth" changes, the "discount" and "parentEmail" properties will be
* updated. You can then hide and show properties on the fly by using the
* displayVariant key:
*
* <pre>
* 100 : ((task = 'create' or task = 'edit') and entity.name = 'Person' and propertyKey = 'parentEmail' and object.isAdult = '1') => displayVariant = "omit" [com.webobjects.directtoweb.Assignment]
* </pre>
*
*
* By default, ERMDAjaxNotificationCenter will be included in the
* aboveDisplayPropertyKeys repetition when propertyDependencies is not null. If
* you set aboveDisplayPropertyKeys yourself, you have to include the
* "ajaxNotificationCenter" property key.
*
* Unlike the original version by Ramsey, this implementation depends on
* ERMDInspectPageRepetition to insert AjaxObserveField and AjaxUpdateContainer
* components where required.
*
* @d2wKey ajaxNotificationCenter
* @d2wKey propertyDependencies
*
* @author rgurley@mac.com
* @author fpeters
*
*/
public class ERMDAjaxNotificationCenter extends ERDCustomComponent {
private static final long serialVersionUID = 1L;
public static final ERXKey<String> AJAX_NOTIFICATION_CENTER_ID = new ERXKey<String>(
"ajaxNotificationCenterID");
public static final ERXKey<String> PROPERTY_OBSERVER_ID = new ERXKey<String>(
"propertyObserverID");
public static final ERXKey<String> PROPERTY_KEY = new ERXKey<String>("propertyKey");
public static final ERXKey<NSDictionary<String, NSArray<String>>> PROPERTY_DEPENDENCIES = new ERXKey<NSDictionary<String, NSArray<String>>>(
"propertyDependencies");
public static final String PropertyChangedNotification = "PropertyChangedNotification";
public static final String RegisterPropertyObserverIDNotification = "RegisterPropertyObserverIDNotification";
@SuppressWarnings("rawtypes")
private NSSelector propertyChanged = new NSSelector("propertyChanged",
ERXConstant.NotificationClassArray);
private String id;
private NSMutableArray<String> updateContainerIDs = new NSMutableArray<String>();
private static final Logger log = LoggerFactory.getLogger(ERMDAjaxNotificationCenter.class);
public String id() {
if (id == null) {
id = ERXWOContext.safeIdentifierName(context(), true);
AJAX_NOTIFICATION_CENTER_ID.takeValueInObject(id, d2wContext());
}
return id;
}
public ERMDAjaxNotificationCenter(WOContext context) {
super(context);
}
public void setD2wContext(D2WContext context) {
if (context != null && !context.equals(d2wContext())) {
log.debug("Removing observers for old context");
NSNotificationCenter.defaultCenter().removeObserver(this,
PropertyChangedNotification, null);
}
NSNotificationCenter.defaultCenter().addObserver(this, propertyChanged,
PropertyChangedNotification, context);
log.debug("Notifications registered for context: {}", context);
super.setD2wContext(context);
}
public NSMutableArray<String> updateContainerIDs() {
log.debug("Updating container IDs: {}", updateContainerIDs.componentsJoinedByString(", "));
return updateContainerIDs;
}
public void propertyChanged(NSNotification n) {
log.debug("Property changed for property key: {}", PROPERTY_KEY.valueInObject(n.object()));
NSArray<String> updateProps = propertyChanged((D2WContext) n.object());
if (updateProps != null && updateProps.count() > 0) {
refreshRelationships(updateProps);
// collect the corresponding update container IDs
NSMutableArray<String> attributeLineUCs = new NSMutableArray<String>();
D2WContext c = (D2WContext) n.object();
String pageConfiguration = (String) c.valueForKey("pageConfiguration");
for (String aPropertyName : updateProps) {
String lineUC = pageConfiguration;
lineUC = lineUC.concat(ERXStringUtilities.capitalize(aPropertyName));
lineUC = lineUC.concat("LineUC");
attributeLineUCs.addObject(lineUC);
}
ERXArrayUtilities.addObjectsFromArrayWithoutDuplicates(updateContainerIDs,
attributeLineUCs);
// force update of notification center UC
AjaxUpdateContainer.safeUpdateContainerWithID(id, context());
log.debug("Container ids to be updated: {}", updateContainerIDs.componentsJoinedByString(", "));
}
}
public NSDictionary<String, NSArray<String>> propertyDependencies(D2WContext context) {
NSDictionary<String, NSArray<String>> propertyDependencies = PROPERTY_DEPENDENCIES
.valueInObject(context);
return propertyDependencies;
}
/**
* @param context
* The d2wContext of the changed property level component
* @return a list of property keys to be updated
*/
@SuppressWarnings("unchecked")
public NSArray<String> propertyChanged(D2WContext context) {
String prop = context.propertyKey();
NSArray<String> dependants = NSArray.EmptyArray;
NSDictionary<String, NSArray<String>> propertyDependencies = PROPERTY_DEPENDENCIES
.valueInObject(context);
if (propertyDependencies.containsKey(prop)) {
dependants = (NSArray<String>) propertyDependencies.valueForKey(prop);
}
return dependants;
}
/**
* Sends out a notification to instances of
* {@link ERMODEditRelationshipPage} for any relationships that have to be
* updated, causing them to be refetched.
*
* @param updateProps
*/
private void refreshRelationships(NSArray<String> updateProps) {
for (String aPropertyKey : updateProps) {
// TODO handle key paths to different entities
if (d2wContext().entity().relationshipNamed(aPropertyKey) != null) {
// this is a relationship, so we'll send out a notification
Object obj = context().page();
String OBJECT_KEY = "object";
NSMutableDictionary<String, Object> userInfo = new NSMutableDictionary<String, Object>(
obj, OBJECT_KEY);
userInfo.setObjectForKey(d2wContext().valueForKey("object"), OBJECT_KEY);
userInfo.setObjectForKey(aPropertyKey, "propertyKey");
userInfo.setObjectForKey(id, "ajaxNotificationCenterId");
// HACK: the delete action notification is the only way to
// trigger a relationship component update for now
NSNotificationCenter.defaultCenter().postNotification(
ERMDDeleteButton.BUTTON_PERFORMED_DELETE_ACTION, obj, userInfo);
log.debug("Sent update notification for relationship: {}", aPropertyKey);
}
}
}
/**
* Since this component uses synchronization to update observers when the
* d2wContext changes, it cannot be non-synchronizing. However, if we want
* to be able to drop this component anywhere, it needs to be able to accept
* any binding value. So this method simply returns value for key from the
* dynamicBindings dictionary.
*/
public Object handleQueryWithUnboundKey(String key) {
log.debug("Handling unbound key: {}", key);
return dynamicBindings().objectForKey(key);
}
/**
* Since this component uses synchronization to update observers when the
* d2wContext changes, it cannot be non-synchronizing. However, if we want
* to be able to drop this component anywhere, it needs to be able to accept
* any binding value. So this method simply adds value for key to the
* dynamicBindings dictionary.
*/
@SuppressWarnings("unchecked")
public void handleTakeValueForUnboundKey(Object value, String key) {
log.debug("Take value: {} for unbound key: {}", value, key);
dynamicBindings().setObjectForKey(value, key);
}
}