/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file. */
package er.directtoweb.pages;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOApplication;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver.WOResponse;
import com.webobjects.directtoweb.D2WContext;
import com.webobjects.directtoweb.D2WModel;
import com.webobjects.directtoweb.D2WPage;
import com.webobjects.directtoweb.InspectPageInterface;
import com.webobjects.directtoweb.NextPageDelegate;
import com.webobjects.eocontrol.EODataSource;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSTimestamp;
import com.webobjects.foundation._NSUtilities;
import er.directtoweb.ERD2WContainer;
import er.directtoweb.ERDirectToWeb;
import er.directtoweb.delegates.ERDBranchDelegate;
import er.directtoweb.delegates.ERDBranchDelegateInterface;
import er.directtoweb.delegates.ERDBranchInterface;
import er.directtoweb.interfaces.ERDUserInfoInterface;
import er.extensions.ERXExtensions;
import er.extensions.appserver.ERXComponentActionRedirector;
import er.extensions.components.ERXClickToOpenSupport;
import er.extensions.components.ERXComponentUtilities;
import er.extensions.eof.ERXGuardedObjectInterface;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.foundation.ERXValueUtilities;
import er.extensions.localization.ERXLocalizer;
import er.extensions.statistics.ERXStats;
import er.extensions.validation.ERXExceptionHolder;
import er.extensions.validation.ERXValidation;
import er.extensions.validation.ERXValidationException;
/**
* Common superclass for all ERD2W templates (except ERD2WEditRelationshipPage).
* Has tons of extra functionality:
* <ul>
* <li>Debugging support.<br>
* Special handlers add extra info in the request-response loop</li>
* <li>Workflow extensions.<br>
* If your NextPageDelegate is a {@link ERDBranchDelegate}, then all of the
* code for actions can be handled in your delegate.</li>
* <li>Display key extensions. We support tab and sectioned pages via the
* d2wContext array.</li>
* </ul>
* In the case of a non-tab page, we expect d2wContext.sectionsContents to
* return one of the three following formats: (( section1, key1, key2, key4 ), (
* section2, key76, key 5, ..) .. ) OR with the sections enclosed in "()" - this
* is most useful with the WebAssistant ( "(section1)", key1, key2, key3,
* "(section2)", key3, key4, key5... ) OR with normal displayPropertyKeys array
* in fact if sectionContents isn't found then it will look for
* displayPropertyKeys ( key1, key2, key3, ... )
*
* In the case of a TAB page, we expect d2wContext.tabSectionsContents to return
* one of the two following formats: ( ( tab1, key1, key2, key4 ), ( tab2,
* key76, key 5, ..) .. ) OR with sections ( ( tab1, ( section1, key1, key2 ..),
* (section3, key4, key..) ), ... ) OR with the alternate syntax, which is most
* useful with the WebAssistant ( "[tab1]", "(section1)", key1, key2, ...
* "[tab2]", "(section3)", key4, key..... )
* @d2wKey object
* @d2wKey localContext
* @d2wKey keyPathsWithValidationExceptions
* @d2wKey shouldPropagateExceptions
* @d2wKey shouldCollectionValidationExceptions
* @d2wKey shouldSetFailedValidationValue
* @d2wKey errorMessages
* @d2wKey componentName
* @d2wKey customComponentName
* @d2wKey propertyKey
* @d2wKey sectionKey
* @d2wKey sectionContents
* @d2wKey tabKey
* @d2wKey displayNameForTabKey
* @d2wKey isEntityEditable
* @d2wKey pageConfiguration
* @d2wKey displayPropertyKeys
* @d2wKey tabSectionsContents
* @d2wKey displayVariant
* @d2wKey displayNameForEntity
* @d2wKey nextPageDelegate
* @d2wKey pageController
* @d2wKey pageWrapperName
* @d2wKey inlineStyle
*/
public abstract class ERD2WPage extends D2WPage implements ERXExceptionHolder, ERDUserInfoInterface, ERXComponentActionRedirector.Restorable, ERDBranchInterface {
/**
* 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;
/** interface for all the keys used in this pages code */
public static interface Keys {
public static final String object = "object";
public static final String localContext = "localContext";
public static final String d2wContext = "d2wContext";
public static final String keyPathsWithValidationExceptions = "keyPathsWithValidationExceptions";
public static final String shouldPropagateExceptions = "shouldPropagateExceptions";
public static final String shouldCollectValidationExceptions = "shouldCollectValidationExceptions";
public static final String shouldSetFailedValidationValue = "shouldSetFailedValidationValue";
public static final String errorMessages = "errorMessages";
public static final String componentName = "componentName";
public static final String customComponentName = "customComponentName";
public static final String pageConfiguration = "pageConfiguration";
public static final String propertyKey = "propertyKey";
public static final String sectionKey = "sectionKey";
public static final String sectionsContents = "sectionsContents";
public static final String tabKey = "tabKey";
public static final String tabIndex = "tabIndex";
public static final String tabCount = "tabCount";
public static final String displayNameForTabKey = "displayNameForTabKey";
public static final String displayPropertyKeys = "displayPropertyKeys";
public static final String tabSectionsContents = "tabSectionsContents";
public static final String alternateKeyInfo = "alternateKeyInfo";
public static final String displayVariant = "displayVariant";
public static final String clickToOpenEnabled = "clickToOpenEnabled";
// The propertyKey whose form widget gets the focus upon loading an edit page.
public static final String firstResponderKey = "firstResponderKey";
}
/** logging support */
public final static Logger log = Logger.getLogger(ERD2WPage.class);
public static final Logger validationLog = Logger.getLogger("er.directtoweb.validation.ERD2WPage");
private String _statsKeyPrefix;
/**
* Default public constructor.
*
* @param c
* current context.
*/
public ERD2WPage(WOContext c) {
super(c);
}
/**
* Overridden to lock the page's editingContext, if there is any present.
*/
@Override
public void awake() {
super.awake();
if (_context != null) {
_context.lock();
}
}
/**
* Returns whether or not click-to-open should be enabled for this component. By
* default this returns ERXClickToOpenSupport.isEnabled().
*
* @param response the response
* @param context the context
* @return whether or not click-to-open is enabled for this component
*/
public boolean clickToOpenEnabled(WOResponse response, WOContext context) {
return ERXValueUtilities.booleanValueWithDefault(d2wContext().valueForKey(Keys.clickToOpenEnabled), ERXClickToOpenSupport.isEnabled());
}
/**
* Utility method to get a value from the user prefs.
*
* @param key
*/
protected Object userPreferencesValueForKey(String key) {
Object result = null;
NSKeyValueCoding userPreferences = (NSKeyValueCoding) d2wContext().valueForKey("userPreferences");
if (userPreferences != null) {
result = userPreferences.valueForKey(key);
}
return result;
}
/**
* Utility method to get a value for the current page configuration from the
* user prefs.
*
* @param key
*/
protected Object userPreferencesValueForPageConfigurationKey(String key) {
key = ERXExtensions.userPreferencesKeyFromContext(key, d2wContext());
return userPreferencesValueForKey(key);
}
/**
* Overridden to unlock the page's editingContext, if there is any present.
*/
@Override
public void sleep() {
if (_context != null) {
_context.unlock();
}
// Make sure the property key is cleared out. In some embedded page configurations, invoking an action in the embedded component
// interrupts the repetition over the property keys, preventing the nullification of the value at the end of the repetition. This causes
// weird stuff to happen.
d2wContext().takeValueForKey(null, "propertyKey");
super.sleep();
}
/**
* Sets the page's editingContext, automatically locking/unlocking it.
*
* @param newEditingContext
* new EOEditingContext
*/
public void setEditingContext(EOEditingContext newEditingContext) {
if (newEditingContext != _context) {
if (_context != null) {
_context.unlock();
}
_context = newEditingContext;
if (_context != null) {
_context.lock();
}
}
}
public EOEditingContext editingContext() {
return _context;
}
/**
* Returns true if the EC has "real" changes (processRecentChanges was called)
*/
public boolean hasActualChanges() {
EOEditingContext ec = editingContext();
boolean hasChanges = ec.hasChanges();
if(hasChanges) {
hasChanges = ec.insertedObjects().count() > 0;
hasChanges |= ec.updatedObjects().count() > 0 && ((NSArray)ec.updatedObjects().valueForKeyPath("changesFromCommittedSnapshot.allValues.@flatten")).count() > 0;
hasChanges |= ec.deletedObjects().count() > 0;
}
return hasChanges;
}
/**
* Implementation of the {@link er.extensions.appserver.ERXComponentActionRedirector.Restorable}
* interface. This implementation creates an URL with the name of the
* current pageConfiguration as a direct action, which assumes a
* {@link er.directtoweb.ERD2WDirectAction} as the default direct action. Subclasses need
* to implement more sensible behaviour.
*
* @return url for the current page
*/
public String urlForCurrentState() {
return context().directActionURLForActionNamed(d2wContext().dynamicPage(), null).replaceAll("&", "&");
}
/** {@link EOEditingContext} for the current object */
protected EOEditingContext _context;
/** Implementation of the {@link InspectPageInterface} */
@Override
public void setObject(EOEnterpriseObject eo) {
setEditingContext((eo != null) ? eo.editingContext() : null);
/*
* Storing the EO in the D2WComponent field prevents serialization. The
* ec must be serialized before the EO. So we store the value in the
* context instead.
*
* also, for SmartAssignment
*/
d2wContext().takeValueForKey(eo, Keys.object);
}
/**
* Return the object from the d2wContext.
*/
@Override
public EOEnterpriseObject object() {
return (EOEnterpriseObject) d2wContext().valueForKey(Keys.object);
}
@Override
public void setDataSource(EODataSource eodatasource) {
setEditingContext(eodatasource != null ? eodatasource.editingContext() : null);
super.setDataSource(eodatasource);
}
/** Can be used to get this instance into KVC */
public final WOComponent self() {
return this;
}
/**
* {@link D2WContext} for this page. Checks if there is a "d2wContext"
* binding, too.
*
* @return d2wContext
*/
@Override
public D2WContext d2wContext() {
if (super.d2wContext() == null) {
if (hasBinding(Keys.localContext)) {
setLocalContext((D2WContext) valueForBinding(Keys.localContext));
}
}
return super.d2wContext();
}
/** Key-Value-Coding needs this method. It should not be called */
public void setD2wContext(D2WContext newValue) {
}
/** Sets the d2wContext for this page */
@Override
public void setLocalContext(D2WContext newValue) {
if (ObjectUtils.notEqual(newValue, _localContext)) {
// HACK ALERT: this next line is made necessary by the
// brain-damageness of
// D2WComponent.setLocalContext, which holds on to the first non
// null value it gets.
// I swear if I could get my hands on the person who did that.. :-)
_localContext = newValue;
log.debug("SetLocalContext: " + newValue);
}
super.setLocalContext(newValue);
if (newValue != null)
newValue.takeValueForKey(keyPathsWithValidationExceptions, Keys.keyPathsWithValidationExceptions);
else
log.warn("D2WContext was null!");
}
// **************************************************************************
// Error handling extensions
// **************************************************************************
protected NSMutableDictionary errorMessages = new NSMutableDictionary();
protected NSMutableArray errorKeyOrder = new NSMutableArray();
protected NSMutableArray<String> keyPathsWithValidationExceptions = new NSMutableArray<>();
protected String errorMessage = "";
protected ValidationDelegate validationDelegate;
protected boolean validationDelegateInited;
public NSMutableDictionary errorMessages() {
return errorMessages;
}
public void setErrorMessages(NSMutableDictionary value) {
errorMessages = value;
}
public String errorMessage() {
return errorMessage;
}
public void setErrorMessage(String message) {
errorMessage = message == null ? "" : message;
}
public boolean hasErrors() {
return (errorMessages != null && errorMessages.count() > 0) || (errorMessage != null && errorMessage.trim().length() > 0);
}
public NSArray errorKeyOrder() {
return errorKeyOrder;
}
/**
* Should exceptions be propagated through to the parent page. If false, the
* validation errors are not shown at all.
*/
public boolean shouldPropagateExceptions() {
return ERXValueUtilities.booleanValue(d2wContext().valueForKey(Keys.shouldPropagateExceptions));
}
/** Should exceptions also be handled here or only handled by the parent. */
public boolean shouldCollectValidationExceptions() {
return ERXValueUtilities.booleanValue(d2wContext().valueForKey(Keys.shouldCollectValidationExceptions));
}
/**
* Clears all of the collected validation exceptions. Implementation of the
* {@link ERXExceptionHolder} interface.
*/
public void clearValidationFailed() {
errorMessage = null;
errorMessages.removeAllObjects();
errorKeyOrder.removeAllObjects();
keyPathsWithValidationExceptions.removeAllObjects();
if(validationDelegate() != null) {
validationDelegate().clearValidationFailed();
}
}
/**
* Should incorrect values still be set into the EO. If not set, then the
* user must re-enter them.
*/
public boolean shouldSetFailedValidationValue() {
return ERXValueUtilities.booleanValue(d2wContext().valueForKey(Keys.shouldSetFailedValidationValue));
}
/** Used to hold a cleaned-up validation key and message. */
private NSMutableDictionary<String,String> _temp = new NSMutableDictionary<>();
/** Handles validation errors. */
@Override
public void validationFailedWithException(Throwable e, Object value, String keyPath) {
if (validationLog.isDebugEnabled()) {
validationLog.debug("Validation failed with exception: " + e + " value: " + value + " keyPath: " + keyPath);
}
if (shouldCollectValidationExceptions()) {
if(validationDelegate() != null) {
validationDelegate().validationFailedWithException(e, value, keyPath);
return;
}
if (e instanceof ERXValidationException) {
ERXValidationException erv = (ERXValidationException) e;
// DT: if we are using the ERXValidation dictionary in the
// EOModel to define validation rules AND
// if we are using keyPaths like person.firstname instead of
// firstname because we have something like:
// user <<-> person and are editing an user instance then
// without this fix here the ERD2WPropertyKey
// would not recognize that 'his' value failed.
if ("value".equals(keyPath)) {
keyPath = "" + d2wContext().propertyKey();
}
erv.setContext(d2wContext());
if (d2wContext().propertyKey() != null) {
if(!errorKeyOrder.containsObject(d2wContext().displayNameForProperty())) {
errorKeyOrder.addObject(d2wContext().displayNameForProperty());
}
errorMessages.setObjectForKey(erv.getMessage(), d2wContext().displayNameForProperty());
// DT: the propertyKey from the validationException is
// important because keyPath might only be
// saveChangesExceptionKey
// which is not enough
String key = erv.propertyKey();
if (key == null) {
key = d2wContext().propertyKey();
}
keyPathsWithValidationExceptions.addObject(key);
if (erv.eoObject() != null && erv.propertyKey() != null && shouldSetFailedValidationValue()) {
try {
erv.eoObject().takeValueForKeyPath(value, erv.propertyKey());
} catch (NSKeyValueCoding.UnknownKeyException ex) {
// AK: as we could have custom components that have
// non-existant keys
// we of course can't push a value, so we discard
// the resulting exception
} catch (NoSuchElementException ex) {
// AK: as we could have custom components that have
// non-existant keys
// we of course can't push a value, so we discard
// the resulting exception
}
}
}
if(("saveChangesExceptionKey".equals(keyPath) || "queryExceptionKey".equals(keyPath)) && erv.propertyKey() != null) {
// AK: this is for combined keys like company,taxIdentifier
keyPathsWithValidationExceptions.addObjectsFromArray(NSArray.componentsSeparatedByString( erv.propertyKey(), ","));
}
} else {
_temp.removeAllObjects();
ERXValidation.validationFailedWithException(e, value, keyPath, _temp, propertyKey(), ERXLocalizer.currentLocalizer(), d2wContext().entity(),
shouldSetFailedValidationValue());
errorKeyOrder.addObjectsFromArray(_temp.allKeys());
errorMessages.addEntriesFromDictionary(_temp);
}
d2wContext().takeValueForKey(errorMessages, Keys.errorMessages);
if (keyPath != null) {
// this is set when you have multiple keys failing
// your keyPath should look like "foo,bar.baz"
if (keyPath.indexOf(",") > 0) {
keyPathsWithValidationExceptions.addObjectsFromArray(NSArray.componentsSeparatedByString(keyPath, ","));
} else {
keyPathsWithValidationExceptions.addObject(keyPath);
}
}
} else if (parent() != null && shouldPropagateExceptions()) {
parent().validationFailedWithException(e, value, keyPath);
}
}
public ValidationDelegate validationDelegate() {
if(!validationDelegateInited && _localContext != null && shouldCollectValidationExceptions()) {
// initialize validation delegate
String delegateClassName = (String)_localContext.valueForKey("validationDelegateClassName");
if(delegateClassName != null) {
try {
Class<? extends ValidationDelegate> delegateClass =
_NSUtilities.classWithName(delegateClassName);
if(delegateClass != null) {
Constructor<? extends ValidationDelegate> constructor =
delegateClass.getConstructor(ERD2WPage.class);
validationDelegate = constructor.newInstance(this);
}
} catch (NoSuchMethodException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
} catch (IllegalArgumentException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
} catch (InstantiationException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
} catch (IllegalAccessException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
} catch (InvocationTargetException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
}
}
validationDelegateInited = true;
}
return validationDelegate;
}
public void setValidationDelegate(ValidationDelegate delegate) {
validationDelegate = delegate;
}
public static abstract class ValidationDelegate 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;
protected final ERD2WPage _page;
public ValidationDelegate(ERD2WPage page) {
_page = page;
}
protected NSMutableDictionary errorMessages() {
return _page.errorMessages;
}
protected NSMutableArray errorKeyOrder() {
return _page.errorKeyOrder;
}
protected String errorMessage() {
return _page.errorMessage;
}
protected void setErrorMessage(String errorMessage) {
_page.setErrorMessage(errorMessage);
}
public abstract boolean hasValidationExceptionForPropertyKey();
public abstract void validationFailedWithException(Throwable e, Object value, String keyPath);
public abstract void clearValidationFailed();
public abstract String errorMessageForPropertyKey();
}
/**
* @return the validation exception message for the current property key
*/
public String errorMessageForPropertyKey() {
if(validationDelegate() != null) {
return validationDelegate().errorMessageForPropertyKey();
}
return propertyKey() != null && keyPathsWithValidationExceptions.containsObject(propertyKey())?
(String) errorMessages().objectForKey(propertyKey()):null;
}
/** Checks if the current object can be edited. */
public boolean isObjectEditable() {
boolean result = !isEntityReadOnly();
Object o = object();
if (o instanceof ERXGuardedObjectInterface) {
result = result && ((ERXGuardedObjectInterface) o).canUpdate();
}
return result;
}
/** Checks if the current object can be deleted. */
public boolean isObjectDeleteable() {
boolean result = !isEntityReadOnly();
Object o = object();
if (o instanceof ERXGuardedObjectInterface) {
result = result && ((ERXGuardedObjectInterface) o).canDelete();
}
return result;
}
/** Checks if the current object can be viewed. */
public boolean isObjectInspectable() {
return true;
}
/**
* True if the entity is read only. Returns
* <code>!(isEntityEditable()) </code>
*/
@Override
public boolean isEntityReadOnly() {
return !isEntityEditable();
}
/**
* If the key <code>isEntityEditable</code> is set, then this value is
* used, otherwise the value from the super implementation, which checks if
* the entity is not in the list of <code>readOnlyEntityNames</code>.
*
*/
public boolean isEntityEditable() {
return ERXValueUtilities.booleanValueWithDefault(d2wContext().valueForKey("isEntityEditable"), !super.isEntityReadOnly());
}
/**
* Checks if there is a validation exception in the D2WContext for the
* current property key.
*/
public boolean hasValidationExceptionForPropertyKey() {
if(validationDelegate() != null) {
return validationDelegate().hasValidationExceptionForPropertyKey();
}
return d2wContext().propertyKey() != null && keyPathsWithValidationExceptions.count() != 0 ? keyPathsWithValidationExceptions.containsObject(d2wContext().propertyKey())
: false;
}
// **************************************************************************
// Debugging extensions
// **************************************************************************
/** Holds the user info. */
protected NSMutableDictionary _userInfo = new NSMutableDictionary();
/** Implementation of the {@link ERDUserInfoInterface} */
public NSMutableDictionary userInfo() {
return _userInfo;
}
/** Checks if basic debugging is on */
public boolean d2wDebuggingEnabled() {
return ERDirectToWeb.d2wDebuggingEnabled(session());
}
/** Checks is component names should be shown. */
public boolean d2wComponentNameDebuggingEnabled() {
return ERDirectToWeb.d2wComponentNameDebuggingEnabled(session());
}
/**
* Helper to return the actual current component name, even when wrapped in
* a custom component.
*/
public String d2wCurrentComponentName() {
String name = (String) d2wContext().valueForKey(Keys.componentName);
if (name.indexOf("CustomComponent") >= 0) {
name = (String) d2wContext().valueForKey(Keys.customComponentName);
}
return name;
}
/**
* This will allow d2w pages to be listed on a per configuration basis in
* stats collecting.
*/
@Override
public String descriptionForResponse(WOResponse aResponse, WOContext aContext) {
String descriptionForResponse = (String) d2wContext().valueForKey(Keys.pageConfiguration);
/*
* if (descriptionForResponse == null) log.info("Unable to find
* pageConfiguration in d2wContext: " + d2wContext());
*/
return descriptionForResponse != null ? descriptionForResponse : super.descriptionForResponse(aResponse, aContext);
}
/**
* Overridden from the parent for better logging. Also clears validation
* errors
*/
@Override
public void takeValuesFromRequest(WORequest r, WOContext c) {
// Need to make sure that we have a clean plate, every time
clearValidationFailed();
NDC.push("Page: " + getClass().getName() + (d2wContext() != null ? (" - Configuration: " + d2wContext().valueForKey(Keys.pageConfiguration)) : ""));
try {
super.takeValuesFromRequest(r, c);
} finally {
NDC.pop();
}
}
/** Overridden from the parent for better logging. */
@Override
public WOActionResults invokeAction(WORequest r, WOContext c) {
WOActionResults result = null;
NDC.push("Page: " + getClass().getName() + (d2wContext() != null ? (" - Configuration: " + d2wContext().valueForKey(Keys.pageConfiguration)) : ""));
try {
result = super.invokeAction(r, c);
} finally {
NDC.pop();
}
return result;
}
protected static final NSMutableSet<String> _allConfigurations = new NSMutableSet<>();
/**
* Collects the names of all page configurations as you walk through your
* application.
*
*/
public static NSArray<String> allConfigurationNames() {
synchronized (_allConfigurations) {
return _allConfigurations.allObjects();
}
}
/**
* Overridden from the parent for better logging. Reports exceptions in the
* console for easier debugging.
*/
@Override
public void appendToResponse(WOResponse response, WOContext context) {
String info = "(" + d2wContext().dynamicPage() + ")";
// String info = "(" + getClass().getName() + (d2wContext() != null ? ("/" + d2wContext().valueForKey(Keys.pageConfiguration)) : "") + ")";
NDC.push(info);
if (d2wContext() != null && !WOApplication.application().isCachingEnabled()) {
synchronized (_allConfigurations) {
if (d2wContext().dynamicPage() != null) {
_allConfigurations.addObject(d2wContext().dynamicPage());
}
// log.info("" + NSPropertyListSerialization.stringFromPropertyList(_allConfigurations));
}
}
boolean clickToOpenEnabled = clickToOpenEnabled(response, context);
ERXClickToOpenSupport.preProcessResponse(response, context, clickToOpenEnabled);
super.appendToResponse(response, context);
ERXClickToOpenSupport.postProcessResponse(getClass(), response, context, clickToOpenEnabled);
NDC.pop();
}
// **************************************************************************
// Workflow extensions (Branches)
// **************************************************************************
/** holds the chosen branch */
protected NSDictionary _branch;
/**
* Cover method for getting the choosen branch.
*
* @return user choosen branch.
*/
public NSDictionary branch() {
return _branch;
}
/**
* Sets the user chosen branch.
*
* @param branch
* chosen by user.
*/
public void setBranch(NSDictionary branch) {
_branch = branch;
// Propagate the branchName to the D2WContext.
Object branchValue = _branch != null ? _branch.valueForKey(ERDBranchDelegate.BRANCH_NAME) : NSKeyValueCoding.NullValue;
d2wContext().takeValueForKey(branchValue, ERDBranchDelegate.BRANCH_NAME);
}
/**
* Implementation of the {@link ERDBranchDelegate ERDBranchDelegate}. Gets
* the user selected branch name.
*
* @return user selected branch name.
*/
// ENHANCEME: Should be localized
public String branchName() {
if (branch() != null) {
return (String) branch().valueForKey(ERDBranchDelegate.BRANCH_NAME);
}
return null;
}
/**
* Calculates the branch choices for the current page. This method is just a
* cover for calling the method <code>branchChoicesForContext</code> on
* the current {@link er.directtoweb.delegates.ERDBranchDelegate ERDBranchDelegate}.
*
* @return array of branch choices
*/
public NSArray branchChoices() {
NSArray branchChoices = null;
if (nextPageDelegate() != null && nextPageDelegate() instanceof ERDBranchDelegateInterface) {
branchChoices = ((ERDBranchDelegateInterface) nextPageDelegate()).branchChoicesForContext(d2wContext());
} else if (pageController() != null && pageController() instanceof ERDBranchDelegateInterface) {
branchChoices = pageController().branchChoicesForContext(d2wContext());
} else {
branchChoices = NSArray.EmptyArray;
//log.error("Attempting to call branchChoices on a page with a delegate: " + nextPageDelegate() + " that doesn't support the ERDBranchDelegateInterface!");
}
return branchChoices;
}
/**
* Determines if this message page should display branch choices.
*
* @return if the current delegate supports branch choices.
*/
public boolean hasBranchChoices() {
return nextPageDelegate() != null && nextPageDelegate() instanceof ERDBranchDelegateInterface;
}
// **************************************************************************
// Display property key extensions (Sections)
// **************************************************************************
/** Holds the current section of display keys. */
private ERD2WContainer _currentSection;
/** The current section of display keys. */
public ERD2WContainer currentSection() {
return _currentSection;
}
/** Sets the current section of display keys. */
public void setCurrentSection(ERD2WContainer value) {
_currentSection = value;
if (value != null) {
d2wContext().takeValueForKey(value.name, Keys.sectionKey);
// we can fire rules from the WebAssistant when we push it the
// -remangled sectionName into the context
d2wContext().takeValueForKey("(" + value.name + ")", Keys.propertyKey);
if (log.isDebugEnabled())
log.debug("Setting sectionKey: " + value.name);
}
}
/**
* The display keys for the current section. You bind to this method.
*
* @return array of {@link er.directtoweb.ERD2WContainer} holding the keys for the current
* section
*/
public NSArray currentSectionKeys() {
if (log.isDebugEnabled())
log.debug("currentSectionKeys()");
NSArray keys = (NSArray) d2wContext().valueForKey(Keys.alternateKeyInfo);
if (log.isDebugEnabled())
log.debug("currentSectionKeys (from alternateKeyInfo):" + keys);
keys = keys == null ? (NSArray) currentSection().keys : keys;
if (log.isDebugEnabled())
log.debug("Setting sectionKey and keys: " + _currentSection.name + keys);
return keys;
}
/** Holds the section info */
private NSMutableArray _sectionsContents;
/**
* The array of sections. You bind to this method.
*
* @return array of arrays of {@link er.directtoweb.ERD2WContainer} holding the keys.
*/
public NSArray sectionsContents() {
if (_sectionsContents == null) {
NSArray sectionsContentsFromRule = (NSArray) d2wContext().valueForKey(Keys.sectionsContents);
if (sectionsContentsFromRule == null) {
sectionsContentsFromRule = (NSArray) d2wContext().valueForKey(Keys.displayPropertyKeys);
}
if (sectionsContentsFromRule == null)
throw new RuntimeException("Could not find sectionsContents or displayPropertyKeys in " + d2wContext());
_sectionsContents = ERDirectToWeb.convertedPropertyKeyArray(sectionsContentsFromRule, '(', ')');
}
return _sectionsContents;
}
// **************************************************************************
// Display property key extensions (Tabs)
// **************************************************************************
/** Holds the array of {@link er.directtoweb.ERD2WContainer} defining the tabs. */
private NSArray _tabSectionsContents;
/**
* Returns the array of {@link er.directtoweb.ERD2WContainer} defining the tabs. A tab is a
* key and an array of sections
*/
public NSArray<ERD2WContainer> tabSectionsContents() {
if (_tabSectionsContents == null) {
NSArray tabSectionContentsFromRule = (NSArray) d2wContext().valueForKey("tabSectionsContents");
if (tabSectionContentsFromRule == null)
tabSectionContentsFromRule = (NSArray) d2wContext().valueForKey(Keys.displayPropertyKeys);
if (tabSectionContentsFromRule == null)
throw new RuntimeException("Could not find tabSectionsContents in " + d2wContext());
String statsKey = makeStatsKey("ComputeTabSectionsContents");
ERXStats.markStart("D2W", statsKey);
_tabSectionsContents = tabSectionsContentsFromRuleResult(tabSectionContentsFromRule);
ERXStats.markEnd("D2W", statsKey);
// Once calculated we then determine any displayNameForTabKey
String currentTabKey = (String) d2wContext().valueForKey(Keys.tabKey);
for (Enumeration e = _tabSectionsContents.objectEnumerator(); e.hasMoreElements();) {
ERD2WContainer c = (ERD2WContainer) e.nextElement();
if (c.name.length() > 0) {
d2wContext().takeValueForKey(c.name, Keys.tabKey);
c.displayName = (String) d2wContext().valueForKey(Keys.displayNameForTabKey);
}
if (c.displayName == null)
c.displayName = c.name;
}
d2wContext().takeValueForKey(currentTabKey, Keys.tabKey);
}
return _tabSectionsContents;
}
/**
* If you switch the context out from under a wizard page it will hold onto the keys in the tab
* sections and blow up the next time you use it if the entity has changed. This allows you to
* clear the array so it rebuilds.
*/
protected void clearTabSectionsContents() {
_tabSectionsContents = null;
_currentTab = null;
}
/** Dummy denoting to sections. */
private final static NSArray _NO_SECTIONS = new NSArray("");
/** Returns the sections on the current tab. */
public NSArray sectionsForCurrentTab() {
return currentTab() != null ? currentTab().keys : _NO_SECTIONS;
}
/** Holds the current tab. */
private ERD2WContainer _currentTab;
/** Returns the {@link er.directtoweb.ERD2WContainer} defining the current tab. */
public ERD2WContainer currentTab() {
String tabName = (String) d2wContext().valueForKey(Keys.tabKey);
if (_currentTab == null && !ERXStringUtilities.stringIsNullOrEmpty(tabName)) {
for (ERD2WContainer aTab : tabSectionsContents()) {
if (tabName.equals(aTab.name)) {
setCurrentTab(aTab);
}
}
}
if (_currentTab == null) {
_currentTab = tabSectionsContents().objectAtIndex(0);
}
return _currentTab;
}
/** Sets the current tab. */
public void setCurrentTab(ERD2WContainer value) {
_currentTab = value;
if (value != null && value.name != null && !value.name.equals("")) {
NSArray<ERD2WContainer> tabs = tabSectionsContents();
Integer count = Integer.valueOf(tabs.count());
Integer index = Integer.valueOf(tabs.indexOf(value));
d2wContext().takeValueForKey(value.name, Keys.tabKey);
d2wContext().takeValueForKey(count, Keys.tabCount);
d2wContext().takeValueForKey(index, Keys.tabIndex);
if (log.isDebugEnabled()) {
log.debug("Setting tabKey: " + value.name);
log.debug("Setting tabCount: " + count);
log.debug("Setting tabIndex: " + index);
}
}
}
/** Helper method to calculate the tab key array */
protected static NSArray tabSectionsContentsFromRuleResult(NSArray tabSectionContentsFromRule) {
NSMutableArray tabSectionsContents = new NSMutableArray();
if (tabSectionContentsFromRule.count() > 0) {
Object firstValue = tabSectionContentsFromRule.objectAtIndex(0);
if (firstValue instanceof NSArray) {
for (Enumeration e = tabSectionContentsFromRule.objectEnumerator(); e.hasMoreElements();) {
NSArray tab = (NSArray) e.nextElement();
ERD2WContainer c = new ERD2WContainer();
c.name = (String) tab.objectAtIndex(0);
c.keys = new NSMutableArray();
Object testObject = tab.objectAtIndex(1);
if (testObject instanceof NSArray) { // format #2
for (int i = 1; i < tab.count(); i++) {
NSArray sectionArray = (NSArray) tab.objectAtIndex(i);
ERD2WContainer section = new ERD2WContainer();
section.name = (String) sectionArray.objectAtIndex(0);
section.keys = new NSMutableArray(sectionArray);
section.keys.removeObjectAtIndex(0);
c.keys.addObject(section);
}
} else { // format #1
ERD2WContainer fakeTab = new ERD2WContainer();
fakeTab.name = "";
fakeTab.keys = new NSMutableArray(tab);
fakeTab.keys.removeObjectAtIndex(0);
c.keys.addObject(fakeTab);
}
tabSectionsContents.addObject(c);
}
} else if (firstValue instanceof String) {
tabSectionsContents = ERDirectToWeb.convertedPropertyKeyArray(tabSectionContentsFromRule, '[', ']');
for (Enumeration e = tabSectionsContents.objectEnumerator(); e.hasMoreElements();) {
ERD2WContainer tab = (ERD2WContainer) e.nextElement();
if(tab.displayName == null) {
tab.displayName = "Main";
}
if(tab.name.length() == 0) {
tab.name = "Main";
}
tab.keys = ERDirectToWeb.convertedPropertyKeyArray(tab.keys, '(', ')');
}
}
}
return tabSectionsContents;
}
// (ak) these actually belong to CompactEdit and PrinterFriendlyInspect
// moved them here to avoid too much subclassing
public boolean isEmbedded() {
return ERXComponentUtilities.booleanValueForBinding(this, "isEmbedded", false);
}
/**
* Gets the <code>displayVariant</code> for the current property key. The intention is that the display variant
* allows variation in the display method of property keys without needing different, slightly varying,
* <code>displayPropertyKeys</code> or <code>tabSectionsContents</code> rules. Template support has been added for
* the <code>omit</code> and <code>blank</code> variants. One could imagine others, such as <code>collapsed</code>,
* <code>ajax</code>, etc.
* @return the display variant, if specified
*/
public String displayVariant() {
return (String)d2wContext().valueForKey(Keys.displayVariant);
}
/**
* Determines if display of the current property key should be <code>omitted</code>.
* @return true if key should be omitted
*/
public boolean isKeyOmitted() {
return "omit".equals(displayVariant());
}
/*
* // FIXME: Should be dynamic public String pageTitle() { return "NetStruxr -
* "+d2wContext().valueForKey("displayNameForEntity")+" View"; }
*/
public NSTimestamp now() {
return new NSTimestamp();
}
protected WOComponent _nextPage;
protected NextPageDelegate _nextPageDelegate;
@Override
public WOComponent nextPage() {
return _nextPage;
}
@Override
public void setNextPage(WOComponent wocomponent) {
_nextPage = wocomponent;
}
/**
* Checks if the delegate is present and can be invoked, then returns the
* page from it.
*
*/
protected WOComponent nextPageFromDelegate() {
WOComponent result = null;
NextPageDelegate delegate = nextPageDelegate();
if (delegate != null) {
if (!((delegate instanceof ERDBranchDelegate) && (branchName() == null))) {
// AK CHECKME: we assume here, because nextPage() in
// ERDBranchDelegate is final,
// we can't do something reasonable when none of the branch
// buttons was selected.
// This allows us to throw a branch delegate at any page, even
// when no branch was taken
result = delegate.nextPage(this);
}
}
return result;
}
/**
* Returns the page's {@link NextPageDelegate NextPageDelegate},
* if any, checking for a "nextPageDelegate" binding if no delegate
* has been explicitly set.
* @return The page's next page delegate.
*/
@Override
public NextPageDelegate nextPageDelegate() {
if (_nextPageDelegate == null) {
_nextPageDelegate = (NextPageDelegate) d2wContext().valueForKey("nextPageDelegate");
}
return _nextPageDelegate;
}
@Override
public void setNextPageDelegate(NextPageDelegate nextpagedelegate) {
_nextPageDelegate = nextpagedelegate;
}
/**
* Holds the page controller for this page.
*/
protected ERDBranchDelegateInterface _pageController;
/**
* Returns the pageController for this page. If there is none given yet,
* tries to create one by querying the key "pageController" from the
* d2wContext. The most convenient way to set and use a pageController is
* via the rule system:<pre><code>
* 100: (entity.name='WebSite') and (task = 'list') => pageController = "ListWebSiteController" [er.directtoweb.ERDDelayedObjectCreationAssignment]
* 100: (entity.name='WebSite') => actions = {left = (editAction, controllerAction);}
* 100: (propertyKey = 'controllerAction') => componentName = "ERDControllerButton"
* </code></pre> Then ListWebSiteController would be:<pre><code>
* public class ListWebSiteController extends ERDBranchDelegate {
*
* private WOComponent _sender;
*
* private WOComponent sender() {
* return _sender;
* }
*
* private void setSender(WOComponent aSender) {
* _sender = aSender;
* }
*
* private D2WContext d2wContext() {
* return (D2WContext) sender().valueForKey("d2wContext");
* }
*
* private EOEnterpriseObject object() {
* return (EOEnterpriseObject) d2wContext().valueForKey("object");
* }
*
* // first action, show up as "Copy Web Site"
* public WOComponent copyWebSite(WOComponent sender) {
* setSender(sender);
* WOComponent result = ....
* return result;
* }
*
* // second action, show up as "Delete Web Site"
* public WOComponent deleteWebSite(WOComponent sender) {
* setSender(sender);
* WOComponent result = ....
* return result;
* }
* }
* </code></pre> The nice thing about this is that this allows you to keep your
* logic confined to just a handful of classes, without the need to
* constantly create new components that just handle one action.
*
*/
public ERDBranchDelegateInterface pageController() {
if (_pageController == null) {
_pageController = (ERDBranchDelegateInterface) d2wContext().valueForKey("pageController");
}
return _pageController;
}
public void setPageController(ERDBranchDelegateInterface aPageController) {
_pageController = aPageController;
}
@Override
public boolean showCancel() {
return _nextPageDelegate != null || _nextPage != null;
}
public NSDictionary settings() {
String pc = d2wContext().dynamicPage();
if(pc != null) {
return new NSDictionary(pc, "parentPageConfiguration");
}
return null;
}
/**
* Gets the name of the page wrapper component. Overrides the superclass' implementation which caches the
* name too aggressively. This method allows you to drive the page wrapper component via the rules.
* Defaults to <code>PageWrapper</code> if a value is not found from the D2W rules.
*
* @return the name of the page wrapper
*/
@Override
public String pageWrapperName() {
String name = (String)d2wContext().valueForKey(D2WModel._PageWrapperNameKey);
if (null == name) {
name = "PageWrapper";
}
return name;
}
/**
* This variant of pageWithName provides a Java5 genericized version of the
* original pageWithName. You would call it with:
*
* MyNextPage nextPage = pageWithName(MyNextPage.class);
*
* @param <T>
* the type of component to create
* @param componentClass
* the Class of the component to load
* @return an instance of the requested component class
*/
@SuppressWarnings("unchecked")
public <T extends WOComponent> T pageWithName(Class<T> componentClass) {
return (T) super.pageWithName(componentClass.getName());
}
public boolean isTopLevelPage() {
return this == topLevelPage();
}
/**
* Gets the top level D2WPage.
* @return the page
*/
private ERD2WPage topLevelPage() {
ERD2WPage page = this;
boolean hasParentPage = true;
while (hasParentPage) {
WOComponent component = page.parent();
// Try to get the next ERD2WPage up the chain.
while (component != null && !(component instanceof ERD2WPage)) {
component = component.parent();
}
if (null == component) {
hasParentPage = false;
} else {
page = (ERD2WPage)component;
}
}
return page;
}
/**
* Determines whether the component should display the detailed "inline" page metrics.
* @return true if should show the detailed metrics
*/
public boolean shouldDisplayDetailedPageMetrics() {
return ERDirectToWeb.pageMetricsEnabled() && ERDirectToWeb.detailedPageMetricsEnabled();
}
/**
* Determines whether the component should display the page metrics summary. On display the summary for the
* top-level page by default, and for all pages when showing detailed metrics.
* @return true if should show metrics summary
*/
public boolean shouldDisplayPageMetricsSummary() {
boolean metricsEnabled = ERDirectToWeb.pageMetricsEnabled();
return isTopLevelPage() ? metricsEnabled : (metricsEnabled && shouldDisplayDetailedPageMetrics());
}
/**
* Creates a stats key to identify stats to the current property key.
* @return the stats key
*/
public String statsKeyForCurrentPropertyKey() {
return makeStatsKey(propertyKey());
}
/**
* Makes the stats key, prepending a prefix to identify the stats to the originating page.
* @param key to format
* @return the formatted key
*/
protected String makeStatsKey(String key) {
return statsKeyPrefix() + key;
}
/**
* A stats key prefix that guarantees the stats will be identifiable to this instance.
* @return the key prefix
*/
public String statsKeyPrefix() {
if (null == _statsKeyPrefix) {
_statsKeyPrefix = d2wContext().dynamicPage() + "_0x" + hashCode() + ".";
}
return _statsKeyPrefix;
}
/**
* Gets the latest metrics event for the current property key.
* @return the event
*/
public ERXStats.LogEntry latestEntryForCurrentPropertyKey() {
return ERXStats.logEntryForKey(ERXStats.Group.Component, statsKeyForCurrentPropertyKey());
}
/**
* Gets the aggregate duration of events sharing the current property key.
* @return the duration
*/
public long aggregateEventDurationForCurrentPropertyKey() {
return ERXStats.logEntryForKey(ERXStats.Group.Component, statsKeyForCurrentPropertyKey()).sum();
}
/**
* Gets the stats for the current page.
* @return the stats
*/
public NSDictionary statsForPage() {
NSMutableDictionary result = new NSMutableDictionary();
NSDictionary statsDict = ERXStats.statistics();
for (Enumeration keysEnum = statsDict.keyEnumerator(); keysEnum.hasMoreElements();) {
String key = (String)keysEnum.nextElement();
if (key.contains(statsKeyPrefix())) {
String statsGroup = ERXStringUtilities.firstPropertyKeyInKeyPath(key);
NSMutableArray events = (NSMutableArray)result.objectForKey(statsGroup);
if (null == events) {
events = new NSMutableArray();
result.setObjectForKey(events, statsGroup);
}
events.addObject(statsDict.objectForKey(key));
}
}
return result;
}
/**
* Gets the CSS class(es) for the container element, based on the current entity and task.
* @return the css classes
*/
public String cssClassForPageContainerElement() {
NSMutableArray classes = new NSMutableArray();
D2WContext d2wContext = d2wContext();
String task = d2wContext.task();
String subTask = (String)d2wContext.valueForKey("subTask");
String elementClassPrefix = ERXStringUtilities.capitalize(task) + "Table";
classes.addObject(elementClassPrefix);
if (subTask != null) {
classes.addObject(ERXStringUtilities.capitalize(task) + ERXStringUtilities.capitalize(subTask) + "Table");
}
if (d2wContext.dynamicPage() != null && d2wContext.dynamicPage().indexOf("Embedded") > -1) {
classes.addObject(ERXStringUtilities.capitalize(task) + "Embedded");
classes.addObject("embedded");
}
if (entityName() != null) {
classes.addObject(elementClassPrefix + entityName());
}
classes.addObject(elementClassPrefix + d2wContext.dynamicPage());
return classes.componentsJoinedByString(" ");
}
/**
* Gets the CSS class(es) that should be applied to the current property name container element.
* @return the css classes
*/
public String cssClassForPropertyName() {
return _cssClassForTemplateForCurrentPropertyKey("cssClassForPropertyName");
}
/**
* Gets the CSS class(es) that should be applied to the current property key container element.
* @return the css classes
*/
public String cssClassForPropertyKey() {
return _cssClassForTemplateForCurrentPropertyKey("cssClass");
}
/**
* Gets the CSS class(es) that should be applied to the current container element.
* @param cssKey from the d2wContext that defines the CSS for this element
* @return the css classes
*/
private String _cssClassForTemplateForCurrentPropertyKey(String cssKey) {
NSMutableArray classes = new NSMutableArray();
D2WContext d2wContext = d2wContext();
String propertyKey = d2wContext.propertyKey();
if (propertyKey != null) {
classes.addObject(propertyKey.replaceAll("\\.", "_"));
// Required?
if (ERXValueUtilities.booleanValue(d2wContext.valueForKey("displayRequiredMarker")) && !"query".equals(task())) {
classes.addObject("required");
}
// Has error?
if (hasValidationExceptionForPropertyKey()) {
classes.addObject("error");
}
// Explicitly defined class(es).
NSArray explicitClasses = ERXValueUtilities.arrayValueWithDefault(d2wContext.valueForKey(cssKey), NSArray.EmptyArray);
if (explicitClasses.count() > 0) {
classes.addObjectsFromArray(explicitClasses);
}
}
return classes.componentsJoinedByString(" ");
}
/**
* Gets any inline style declarations for the current property name container element.
* @return the inline style declarations
*/
public String inlineStyleDeclarationForPropertyName() {
return (String)d2wContext().valueForKey("inlineStyleForPropertyName");
}
/**
* Gets any inline style declarations for the current property key container element.
* @return the inline style declarations
*/
public String inlineStyleDeclarationForPropertyKey() {
return (String)d2wContext().valueForKey("inlineStyle");
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(d2wContext().valueForKey(Keys.tabKey));
out.writeObject(d2wContext().valueForKey(Keys.tabCount));
out.writeObject(d2wContext().valueForKey(Keys.tabIndex));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
d2wContext().takeValueForKey(in.readObject(), Keys.tabKey);
d2wContext().takeValueForKey(in.readObject(), Keys.tabCount);
d2wContext().takeValueForKey(in.readObject(), Keys.tabIndex);
}
}