package er.extensions.components._private;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOActionResults;
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.WODynamicElementCreationException;
import com.webobjects.appserver._private.WODynamicGroup;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import er.extensions.eof.ERXBatchFetchUtilities;
import er.extensions.eof.ERXConstant;
import er.extensions.eof.ERXDatabaseContextDelegate;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.foundation.ERXValueUtilities;
/**
* Replacement for WORepetition. It is installed via ERXPatcher.setClassForName(ERXWORepetition.class, "WORepetition")
* into the runtime system, so you don't need to reference it explicitly.
* <ul>
* <li>adds support for {@link java.util.List} and arrays (e.g. String[]), in addition to
* {@link com.webobjects.foundation.NSArray} and {@link java.util.Vector} (which is a {@link java.util.List} in 1.4). This
* is listed as Radar #3325342 since June 2003.</li>
* <li>help with backtracking issues by adding not only the current index, but also the current object's hash code to
* the element id, so it looks like "x.y.12345.z".<br>
* If they don't match when invokeAction is called, the list is searched for a matching object. If none is found, then:
* <ul>
* <li>if the property <code>er.extensions.ERXWORepetition.raiseOnUnmatchedObject=true</code> -
* an {@link ERXWORepetition.UnmatchedObjectException} is thrown</li>
* <li>if <code>notFoundMarker</code> is bound, that is used for the item in the repetition. This can be used to flag
* special handling in the action method, possibly useful for Ajax requests</li>
* <li>otherwise, the action is ignored</li>
* </ul>
* This feature is turned on globally if <code>er.extensions.ERXWORepetition.checkHashCodes=true</code> or on a
* per-component basis by setting the <code>checkHashCodes</code> binding to true or false.<br>
* <em>Known issues:</em>
* <ul>
* <li>you can't re-generate your list by creating new objects between the appendToReponse and the next
* takeValuesFromRequest unless you use <code>uniqueKey</code> and the value for that key is consistent across
* the object instances<br>
* When doing this by fetching EOs, this is should not a be problem, as the EO most probably has the same hashCode if
* the EC stays the same. </li>
* <li>Your moved object should still be in the list.</li>
* <li>Form values are currently not fixed, which may lead to NullpointerExceptions or other failures. However, if they
* happen, by default you would have used the wrong values, so it may be arguable that having an error is better...</li>
* </ul>
* </li>
* </ul>
* Note that this implementation adds a small amount of overhead due to the creation of the Context for each RR phase,
* but this is preferable to having to give so many parameters.
*
* As an alternative to the default use of System.identityHashCode to unique your items, you can set the binding "uniqueKey"
* to be a string keypath on your items that can return a unique key for the item. For instance, if you are using
* ERXGenericRecord, you can set uniqueKey = "rawPrimaryKey"; if your EO has an integer primary key, and this will make
* the uniquing value be the primary key instead of the hash code. While this reveals the primary keys of your items,
* the set of possible valid matches is still restricted to only those that were in the list to begin with, so no
* additional capabilities are available to users. <code>uniqueKey</code> does <b>not</b> have to return an integer.
*
* @binding list the array or list of items to iterate over
* @binding item the current item in the iteration
* @binding count the total number of items to iterate over
* @binding index the current index in the iteration
* @binding uniqueKey a String keypath on item (relative to item, not relative to the component) returning a value whose
* toString() is unique for this component
* @binding checkHashCodes if true, checks the validity of repetition references during the RR loop
* @binding raiseOnUnmatchedObject if true, an exception is thrown when the repetition does not find a matching object
* @binding debugHashCodes if true, prints out hashcodes for each entry in the repetition as it is traversed
* @binding batchFetch a comma-separated list of keypaths on the "list" array binding to batch fetch
* @binding eoSupport try to use globalIDs to determine the hashCode for EOs
* @binding notFoundMarker used for the item in the repetition if checkHashCodes is true, don't bind directly to null as
* that will be translated to false
*
* @property er.extensions.ERXWORepetition.checkHashCodes add hash codes to element IDs so backtracking can be controlled
* @property er.extensions.ERXWORepetition.raiseOnUnmatchedObject if an object wasn't found, raise an exception (if unset, the wrong object is used)
* @property er.extensions.ERXWORepetition.eoSupport use hash code of GlobalID instead of object's hash code if it is an EO
*
* @author ak
*/
public class ERXWORepetition extends WODynamicGroup {
private static final Logger log = LoggerFactory.getLogger(ERXWORepetition.class);
protected WOAssociation _list;
protected WOAssociation _item;
protected WOAssociation _count;
protected WOAssociation _index;
protected WOAssociation _uniqueKey;
protected WOAssociation _checkHashCodes;
protected WOAssociation _raiseOnUnmatchedObject;
protected WOAssociation _eoSupport;
protected WOAssociation _debugHashCodes;
protected WOAssociation _batchFetch;
protected WOAssociation _notFoundMarker;
private static boolean _checkHashCodesDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXWORepetition.checkHashCodes", ERXProperties.booleanForKey(ERXWORepetition.class.getName() + ".checkHashCodes"));
private static boolean _raiseOnUnmatchedObjectDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXWORepetition.raiseOnUnmatchedObject", ERXProperties.booleanForKey(ERXWORepetition.class.getName() + ".raiseOnUnmatchedObject"));
private static boolean _eoSupportDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXWORepetition.eoSupport", ERXProperties.booleanForKey(ERXWORepetition.class.getName() + ".eoSupport"));
public static class UnmatchedObjectException extends RuntimeException {
/**
* 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 UnmatchedObjectException() {
}
}
/**
* WOElements must be reentrant, so we need a context object or will have to add the parameters to every method.
* Note that it's OK to have no object at all.
*/
protected static class Context {
protected NSArray<Object> nsarray;
protected List<Object> list;
protected Object[] array;
public Context(Object object) {
if (object != null) {
if (object instanceof NSArray) {
nsarray = (NSArray<Object>) object;
}
else if (object instanceof List) {
list = (List<Object>) object;
}
else if (object instanceof Object[]) {
array = (Object[]) object;
}
else {
throw new IllegalArgumentException("Evaluating 'list' binding returned a " + object.getClass().getName() +
" when it should return either a NSArray, an Object[] array or a java.util.List .");
}
}
}
/**
* Gets the number of elements from any object.
*
* @return size of the list
*/
protected int count() {
if (nsarray != null) {
return nsarray.count();
}
else if (list != null) {
return list.size();
}
else if (array != null) {
return array.length;
}
return 0;
}
/**
* Gets the object at the given index from any object.
*
* @param i index
* @return object at index
*/
protected Object objectAtIndex(int i) {
if (nsarray != null) {
return nsarray.objectAtIndex(i);
}
else if (list != null) {
return list.get(i);
}
else if (array != null) {
return array[i];
}
return null;
}
}
/**
* Designated Constructor. Gets called by the template parser. Checks if the bindings are valid.
*
* @param string
* @param associations
* @param woelement
**/
public ERXWORepetition(String string, NSDictionary<String, WOAssociation> associations, WOElement woelement) {
super(null, null, woelement);
_list = associations.objectForKey("list");
_item = associations.objectForKey("item");
_count = associations.objectForKey("count");
_index = associations.objectForKey("index");
_uniqueKey = associations.objectForKey("uniqueKey");
_checkHashCodes = associations.objectForKey("checkHashCodes");
_raiseOnUnmatchedObject = associations.objectForKey("raiseOnUnmatchedObject");
_debugHashCodes = associations.objectForKey("debugHashCodes");
_eoSupport = associations.objectForKey("eoSupport");
_batchFetch = associations.objectForKey("batchFetch");
_notFoundMarker = associations.objectForKey("notFoundMarker");
if (_list == null && _count == null) {
_failCreation("Missing 'list' or 'count' attribute.");
}
if (_list != null && _item == null) {
_failCreation("Missing 'item' attribute with 'list' attribute.");
}
if (_list != null && _count != null) {
_failCreation("Illegal use of 'count' attribute with 'list' attribute.");
}
if (_count != null && (_list != null || _item != null)) {
_failCreation("Illegal use of 'list', or 'item'attributes with 'count' attribute.");
}
if (_item != null && !_item.isValueSettable()) {
_failCreation("Illegal read-only 'item' attribute.");
}
if (_index != null && !_index.isValueSettable()) {
_failCreation("Illegal read-only 'index' attribute.");
}
}
/**
* Utility to throw an exception if the bindings are incomplete.
*
* @param message
**/
protected void _failCreation(String message) {
throw new WODynamicElementCreationException("<" + getClass().getName() + "> " + message);
}
@Override
public String toString() {
return new StringBuilder().append('<').append(getClass().getName())
.append(" list: ").append(_list)
.append(" item: ").append(_item)
.append(" count: ").append(_count)
.append(" index: ").append(_index).append('>').toString();
}
private int hashCodeForObject(WOComponent component, Object object) {
int hashCode;
if (object == null) {
hashCode = 0;
}
else if (eoSupport(component) && object instanceof EOEnterpriseObject) {
EOEnterpriseObject eo = (EOEnterpriseObject)object;
EOEditingContext editingContext = eo.editingContext();
EOGlobalID gid = null;
if (editingContext != null) {
gid = editingContext.globalIDForObject(eo);
}
// If the EO isn't in an EC, or it has a null GID, then just fall back to the hash code
if (gid == null) {
hashCode = System.identityHashCode(object);
}
else {
hashCode = gid.hashCode();
}
}
else {
hashCode = System.identityHashCode(object);
}
// @see java.lang.Math#abs for an explanation of this
if (hashCode == Integer.MIN_VALUE) {
hashCode = 37; // MS: random prime number
}
hashCode = Math.abs(hashCode);
if (_debugHashCodes != null && _debugHashCodes.booleanValueInComponent(component)) {
log.info("debugHashCodes for '{}', {} = {}", _list.keyPath(), object, hashCode);
}
return hashCode;
}
private String keyForObject(WOComponent component, Object object) {
String uniqueKeyPath = (String)_uniqueKey.valueInComponent(component);
Object uniqueKey = NSKeyValueCodingAdditions.Utility.valueForKeyPath(object, uniqueKeyPath);
if (uniqueKey == null) {
throw new IllegalArgumentException("Can't use null as uniqueKey for " + object);
}
String key = ERXStringUtilities.safeIdentifierName(uniqueKey.toString());
if (_debugHashCodes != null && _debugHashCodes.booleanValueInComponent(component)) {
log.info("debugHashCodes for '{}', {} = {}", _list.keyPath(), object, key);
}
return key;
}
/**
* Prepares the WOContext for the loop iteration.
*
* @param context
* @param index
* @param wocontext
* @param wocomponent
* @param checkHashCodes
*/
protected void _prepareForIterationWithIndex(Context context, int index, WOContext wocontext, WOComponent wocomponent, boolean checkHashCodes) {
Object object = null;
if (_item != null) {
object = context.objectAtIndex(index);
_item._setValueNoValidation(object, wocomponent);
}
if (_index != null) {
Integer integer = ERXConstant.integerForInt(index);
_index._setValueNoValidation(integer, wocomponent);
}
boolean didAppend = false;
if (checkHashCodes) {
if (object != null) {
String elementID = null;
if (_uniqueKey == null) {
int hashCode = hashCodeForObject(wocomponent, object);
if (hashCode != 0) {
elementID = String.valueOf(hashCode);
}
}
else {
elementID = keyForObject(wocomponent, object);
}
if (elementID != null) {
if (index != 0) {
wocontext.deleteLastElementIDComponent();
}
log.debug("prepare {}->{}", elementID, object);
wocontext.appendElementIDComponent(elementID);
didAppend = true;
}
}
}
if (!didAppend) {
if (index != 0) {
wocontext.incrementLastElementIDComponent();
}
else {
wocontext.appendZeroElementIDComponent();
}
}
}
/**
* Cleans the WOContext after the loop iteration.
*
* @param i
* @param wocontext
* @param wocomponent
**/
protected void _cleanupAfterIteration(int i, WOContext wocontext, WOComponent wocomponent) {
if (_item != null) {
_item._setValueNoValidation(null, wocomponent);
}
if (_index != null) {
Integer integer = ERXConstant.integerForInt(i);
_index._setValueNoValidation(integer, wocomponent);
}
wocontext.deleteLastElementIDComponent();
}
/**
* Fills the context with the object given in the "list" binding.
*
* @param senderID
* @param elementID
* @return index string
**/
protected String _indexStringForSenderAndElement(String senderID, String elementID) {
int dotOffset = elementID.length() + 1;
int nextDotOffset = senderID.indexOf('.', dotOffset);
String indexString;
if (nextDotOffset < 0) {
indexString = senderID.substring(dotOffset);
}
else {
indexString = senderID.substring(dotOffset, nextDotOffset);
}
return indexString;
}
protected String _indexOfChosenItem(WORequest worequest, WOContext wocontext) {
String indexString = null;
String senderID = wocontext.senderID();
String elementID = wocontext.elementID();
if (senderID.startsWith(elementID)) {
int i = elementID.length();
if (senderID.length() > i && senderID.charAt(i) == '.')
indexString = _indexStringForSenderAndElement(senderID, elementID);
}
return indexString;
}
protected int _count(Context context, WOComponent wocomponent) {
int count;
if (_list != null) {
count = context.count();
}
else {
Object object = _count.valueInComponent(wocomponent);
if (object != null) {
count = ERXValueUtilities.intValue(object);
}
else {
log.error("{} 'count' evaluated to null in component {}.\nRepetition count reset to 0.", this, wocomponent);
count = 0;
}
}
return count;
}
protected Context createContext(WOComponent wocomponent) {
Object list = (_list != null ? _list.valueInComponent(wocomponent) : null);
if(list instanceof NSArray) {
if (_batchFetch != null) {
String batchFetchKeyPaths = (String)_batchFetch.valueInComponent(wocomponent);
if (batchFetchKeyPaths != null) {
NSArray<String> keyPaths = NSArray.componentsSeparatedByString(batchFetchKeyPaths, ",");
if (keyPaths.count() > 0) {
ERXBatchFetchUtilities.batchFetch((NSArray)list, keyPaths, true);
}
}
}
ERXDatabaseContextDelegate.setCurrentBatchObjects((NSArray)list);
}
return new Context(list);
}
@Override
public void takeValuesFromRequest(WORequest worequest, WOContext wocontext) {
WOComponent wocomponent = wocontext.component();
Context context = createContext(wocomponent);
int count = _count(context, wocomponent);
boolean checkHashCodes = checkHashCodes(wocomponent);
if (log.isDebugEnabled()) {
log.debug("takeValuesFromRequest: {} - {}", wocontext.elementID(), wocontext.request().formValueKeys());
}
for (int index = 0; index < count; index++) {
_prepareForIterationWithIndex(context, index, wocontext, wocomponent, checkHashCodes);
super.takeValuesFromRequest(worequest, wocontext);
}
if (count > 0) {
_cleanupAfterIteration(count, wocontext, wocomponent);
}
}
@Override
public WOActionResults invokeAction(WORequest worequest, WOContext wocontext) {
WOComponent wocomponent = wocontext.component();
Context repetitionContext = createContext(wocomponent);
int count = _count(repetitionContext, wocomponent);
WOActionResults woactionresults = null;
String indexString = _indexOfChosenItem(worequest, wocontext);
int index = 0;
boolean checkHashCodes = checkHashCodes(wocomponent);
if (indexString != null && ! checkHashCodes) {
index = Integer.parseInt(indexString);
}
if (indexString != null) {
if (_item != null) {
Object object = null;
if (checkHashCodes) {
boolean found = false;
if (_uniqueKey == null) {
int hashCode = Integer.parseInt(indexString);
int otherHashCode = 0;
for (int i = 0; i < repetitionContext.count() && !found; i++) {
Object o = repetitionContext.objectAtIndex(i);
otherHashCode = hashCodeForObject(wocomponent, o);
if (otherHashCode == hashCode) {
object = o;
index = i;
found = true;
}
}
if (found) {
log.debug("Found object: {} vs {}", otherHashCode, hashCode);
} else {
log.warn("Wrong object: {} vs {} (array = {})", otherHashCode, hashCode, repetitionContext.nsarray);
}
}
else {
String key = indexString;
String otherKey = null;
for (int i = 0; i < repetitionContext.count() && !found; i++) {
Object o = repetitionContext.objectAtIndex(i);
otherKey = keyForObject(wocomponent, o);
if (otherKey.equals(key)) {
object = o;
index = i;
found = true;
}
}
if (found) {
log.debug("Found object: {} vs {}", otherKey, key);
} else {
log.warn("Wrong object: {} vs {} (array = {})", otherKey, key, repetitionContext.nsarray);
}
}
if (!found) {
if (raiseOnUnmatchedObject(wocomponent)) {
throw new UnmatchedObjectException();
}
if (_notFoundMarker == null) {
return wocontext.page();
}
object = _notFoundMarker.valueInComponent(wocomponent);
}
}
else {
if (index >= repetitionContext.count()) {
if (raiseOnUnmatchedObject(wocomponent)) {
throw new UnmatchedObjectException();
}
return wocontext.page();
}
object = repetitionContext.objectAtIndex(index);
}
_item._setValueNoValidation(object, wocomponent);
}
if (_index != null) {
Integer integer = ERXConstant.integerForInt(index);
_index._setValueNoValidation(integer, wocomponent);
}
wocontext.appendElementIDComponent(indexString);
log.debug("invokeAction: {}", wocontext.elementID());
woactionresults = super.invokeAction(worequest, wocontext);
wocontext.deleteLastElementIDComponent();
}
else {
for (int i = 0; i < count && woactionresults == null; i++) {
_prepareForIterationWithIndex(repetitionContext, i, wocontext, wocomponent, checkHashCodes);
woactionresults = super.invokeAction(worequest, wocontext);
}
if (count > 0) {
_cleanupAfterIteration(count, wocontext, wocomponent);
}
}
return woactionresults;
}
private boolean checkHashCodes(WOComponent wocomponent) {
if (_checkHashCodes != null) {
return _checkHashCodes.booleanValueInComponent(wocomponent);
}
return _checkHashCodesDefault;
}
private boolean raiseOnUnmatchedObject(WOComponent wocomponent) {
if (_raiseOnUnmatchedObject != null) {
return _raiseOnUnmatchedObject.booleanValueInComponent(wocomponent);
}
return _raiseOnUnmatchedObjectDefault;
}
private boolean eoSupport(WOComponent wocomponent) {
if (_eoSupport != null) {
return _eoSupport.booleanValueInComponent(wocomponent);
}
return _eoSupportDefault;
}
@Override
public void appendToResponse(WOResponse woresponse, WOContext wocontext) {
WOComponent wocomponent = wocontext.component();
Context context = createContext(wocomponent);
int count = _count(context, wocomponent);
boolean checkHashCodes = checkHashCodes(wocomponent);
log.debug("appendToResponse: {}", wocontext.elementID());
for (int index = 0; index < count; index++) {
_prepareForIterationWithIndex(context, index, wocontext, wocomponent, checkHashCodes);
appendChildrenToResponse(woresponse, wocontext);
}
if (count > 0) {
_cleanupAfterIteration(count, wocontext, wocomponent);
}
}
}