package er.extensions.components;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver._private.WOKeyValueAssociation;
import com.webobjects.eoaccess.EODatabaseDataSource;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eocontrol.EOArrayDataSource;
import com.webobjects.eocontrol.EODataSource;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation.NSValidation;
import er.extensions.appserver.ERXApplication;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.eof.ERXEOControlUtilities;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.foundation.ERXUtilities;
/**
* This is an effort to consolidate the WOToOneRelationship, WOToManyRelationship and descendant components.
* <p>
* As most of the code between the two is shared anyway, it makes sense to provide a base class and only
* handle the differences in the descendants. One core difference if that this component can handle POJOs both as the
* source and the destination objects. You can't instantiate one of these yourself.
* <p>
* This class can handle to-one, to-many and simple attribute selections. You can can set the list via
* possibleChoices, dataSource, destinationEntityName or via sourceEntityName and relationshipKey.
* <p>
* The main difference between this component and the former WOToOne/WOToMany is that it is non-synchronizing. So if
* you have custom subclasses of WOToOne/WOToMany you need to take this into account.
* Also adds the values that are not included in the restricted-choice list. These items are marked by [name of item].
* This should ensure they end up at the bottom of the list.
* You can also specify the editingContext the component uses to fetch the related objects into.
* NOTE: currently "includeUnmatchedValues" is set to false
* @author ak (but most stuff is pulled over from the pre-existing WOToOne/WOToMany)
*/
public abstract class ERXArrayChooser extends ERXStatelessComponent {
private static final Logger log = LoggerFactory.getLogger(ERXArrayChooser.class);
/**
* 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 static boolean localizeDisplayKeysDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXArrayChooser.localizeDisplayKeysDefault", false);
public static boolean includeUnmatchedValuesDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXArrayChooser.includeUnmatchedValuesDefault", false);
public static boolean sortCaseInsensitiveDefault = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXArrayChooser.sortCaseInsensitive", false);
protected final static String NO_SELECTION_STRING = "ERXArrayChooser.NoSelectionString";
protected final static String NO_SORT_STRING = "WONoSorting";
protected Boolean _localizeDisplayKeys;
protected Boolean _includeUnmatchedValues;
protected Boolean _sortCaseInsensitive;
protected String _sourceEntityName;
protected String _destinationEntityName;
protected String _relationshipKey;
protected Object _sourceObject;
protected String _destinationDisplayKey;
protected EODataSource _dataSource;
protected String _uiStyle;
protected Boolean _isMandatory;
protected NSArray _list;
protected NSArray _unmatchedValues;
protected String _destinationSortKey;
protected String _noneString;
protected Object theCurrentItem;
public ERXArrayChooser(WOContext aContext) {
super(aContext);
}
protected abstract boolean isSingleSelection();
@Override
public void reset() {
super.reset();
_sourceEntityName = null;
_destinationEntityName = null;
_relationshipKey = null;
_sourceObject = null;
_destinationDisplayKey = null;
_dataSource = null;
_uiStyle = null;
_isMandatory = null;
_list = null;
_destinationSortKey = null;
_noneString = null;
_localizeDisplayKeys = null;
_sortCaseInsensitive = null;
_includeUnmatchedValues = null;
_unmatchedValues = null;
}
public String noneString() {
if(_noneString == null) {
_noneString = (String)valueForBinding("noSelectionString");
if(_noneString == null) {
_noneString = "ERXArrayChooser.noneString";
}
if(_noneString != null) {
_noneString = localizer().localizedStringForKeyWithDefault(_noneString);
}
}
return _noneString;
}
public boolean sortCaseInsensitive() {
if(_sortCaseInsensitive == null) {
_sortCaseInsensitive = booleanValueForBinding("sortCaseInsensitive", false) ? Boolean.TRUE : Boolean.FALSE;
}
return _sortCaseInsensitive.booleanValue();
}
public boolean localizeDisplayKeys() {
if(_localizeDisplayKeys == null) {
_localizeDisplayKeys = booleanValueForBinding("localizeDisplayKeys", localizeDisplayKeysDefault) ? Boolean.TRUE : Boolean.FALSE;
}
return _localizeDisplayKeys.booleanValue();
}
public boolean includeUnmatchedValues() {
if(_includeUnmatchedValues == null) {
_includeUnmatchedValues = booleanValueForBinding("includeUnmatchedValues", includeUnmatchedValuesDefault) ? Boolean.TRUE : Boolean.FALSE;
}
return _includeUnmatchedValues.booleanValue();
}
public String sourceEntityName() {
if(_sourceEntityName == null) {
_sourceEntityName = (String)valueForBinding("sourceEntityName");
if (_sourceEntityName == null) {
loadBindingsFromSelection();
}
}
return _sourceEntityName;
}
public String destinationSortKey() {
if (_destinationSortKey == null) {
_destinationSortKey = (String)valueForBinding("destinationSortKey");
if (_destinationSortKey == null|| _destinationSortKey.length() == 0) {
_destinationSortKey = destinationDisplayKey();
} else if (NO_SORT_STRING.equals(_destinationSortKey)) {
_destinationSortKey = null;
}
}
return _destinationSortKey;
}
public NSArray unmatchedValues() {
return _unmatchedValues;
}
protected NSArray destinationSortKeys() {
String destinationSortKey = destinationSortKey();
NSArray destinationSortKeys;
if (destinationSortKey != null) {
destinationSortKeys = NSArray.componentsSeparatedByString(destinationSortKey, ",");
}
else {
destinationSortKeys = NSArray.EmptyArray;
}
return destinationSortKeys;
}
public EOEditingContext editingContext() {
EOEditingContext ec = null;
if(sourceObject() instanceof EOEnterpriseObject) {
ec = ((EOEnterpriseObject)sourceObject()).editingContext();
} else {
ec = (EOEditingContext) valueForBinding("editingContext");
if(ec == null) {
ec = session().defaultEditingContext();
}
}
return ec;
}
/**
* <p>
* I'm lazy. I don't want to bind sourceObject, sourceEntityName, and relationshipKey.
* Work it out, Wonder, that's what I say. So if you bind, for instance:
* </p>
*
* <code>
* selection = person.company;
* </code>
*
* <p>
* ... it will figure out that the sourceObject is "person", the relationshipKey is "company"
* and the sourceEntityName is "Person".
* </p>
*/
protected void loadBindingsFromSelection() {
WOKeyValueAssociation selectionAssociation = (WOKeyValueAssociation)_associationWithName("selection");
if (selectionAssociation != null) {
String selectionKeyPath = selectionAssociation.keyPath();
WOComponent parent = parent();
int lastDotIndex = selectionKeyPath.lastIndexOf('.');
if (lastDotIndex == -1) {
_sourceObject = parent;
_relationshipKey = selectionKeyPath;
}
else {
String sourceObjectKeyPath = selectionKeyPath.substring(0, lastDotIndex);
_sourceObject = parent.valueForKeyPath(sourceObjectKeyPath);
_relationshipKey = selectionKeyPath.substring(lastDotIndex + 1);
if (_sourceObject instanceof EOEnterpriseObject) {
_sourceEntityName = ((EOEnterpriseObject)_sourceObject).entityName();
}
}
}
}
protected EOEntity destinationEntity() {
return ERXEOAccessUtilities.entityNamed(editingContext(), destinationEntityName());
}
public String destinationEntityName() {
return _destinationEntityName(true);
}
public String _destinationEntityName(boolean throwExceptionIfMissing) {
if(_destinationEntityName == null) {
_destinationEntityName = (String)valueForBinding("destinationEntityName");
if(_destinationEntityName == null) {
Object _source = sourceObject();
EOEditingContext ec = editingContext();
EOEntity destinationEntity = null;
if(_source instanceof EOEnterpriseObject) {
EORelationship relationship = ERXUtilities.relationshipWithObjectAndKeyPath((EOEnterpriseObject)_source, relationshipKey());
destinationEntity = relationship != null ? relationship.destinationEntity() : null;
} else {
String anEntityName = sourceEntityName();
if(anEntityName != null) {
EOEntity anEntity = ERXEOAccessUtilities.entityNamed(ec, anEntityName);
if (anEntity == null) {
throw new IllegalStateException("<" + getClass().getName() + " could not find entity named " + anEntityName + ">");
}
destinationEntity = ERXEOAccessUtilities.destinationEntityForKeyPath(anEntity, relationshipKey());
} else {
// MS: This seems bogus -- anEntityName is null here!
destinationEntity = ERXEOAccessUtilities.entityNamed(ec, anEntityName);
}
}
if(destinationEntity == null && throwExceptionIfMissing) {
throw new IllegalStateException("Destination entity could not be retrieved from EO of bindings. Either set the \"sourceObject\" to an EO, provide the \"sourceEntityName\" and \"relationshipKey\", the \"destinationEntityName\" or the \"possibleChoices\" binding.");
}
if (destinationEntity != null) {
_destinationEntityName = destinationEntity.name();
}
}
}
return _destinationEntityName;
}
public String relationshipKey() {
if(_relationshipKey == null) {
_relationshipKey = (String)valueForBinding("relationshipKey");
if (_relationshipKey == null) {
loadBindingsFromSelection();
}
}
return _relationshipKey;
}
public Object sourceObject() {
if(_sourceObject == null) {
_sourceObject = valueForBinding("sourceObject");
if(_sourceObject == null) {
loadBindingsFromSelection();
if (_sourceObject == null) {
throw new IllegalStateException("sourceObject is a required binding.");
}
}
}
return _sourceObject;
}
public String destinationDisplayKey() {
if(_destinationDisplayKey == null) {
_destinationDisplayKey = (String)valueForBinding("destinationDisplayKey");
if(_destinationDisplayKey == null) {
if (_destinationEntityName(false) != null) {
_destinationDisplayKey = "userPresentableDescription";
}
}
}
return _destinationDisplayKey;
}
public EOQualifier qualifier() {
return (EOQualifier)valueForBinding("qualifier");
}
public EODataSource dataSource() {
if(_dataSource == null) {
_dataSource = (EODataSource)valueForBinding("dataSource");
EOQualifier qualifier = qualifier();
if (_dataSource == null) {
String destinationEntityName = destinationEntityName();
EOEditingContext editingContext = editingContext();
if (ERXEOAccessUtilities.entityWithNamedIsShared(editingContext(), destinationEntityName) ) {
EOArrayDataSource arrayDataSource = new EOArrayDataSource(destinationEntity().classDescriptionForInstances(), editingContext);
NSArray sharedEOs = ERXEOControlUtilities.sharedObjectsForEntityNamed(destinationEntityName);
if (sharedEOs == null) {
sharedEOs = NSArray.EmptyArray;
}
else if (qualifier != null) {
sharedEOs = EOQualifier.filteredArrayWithQualifier(sharedEOs, qualifier);
}
arrayDataSource.setArray(sharedEOs);
_dataSource = arrayDataSource;
}
else {
_dataSource = new EODatabaseDataSource(editingContext, destinationEntityName);
if (qualifier != null) {
((EODatabaseDataSource)_dataSource).setAuxiliaryQualifier(qualifier);
}
}
}
else if (qualifier != null) {
throw new IllegalArgumentException("You specified a dataSource binding and a qualifier, which is not currently supported.");
}
}
return _dataSource;
}
public String uiStyle() {
if(_uiStyle == null) {
_uiStyle = (String)valueForBinding("uiStyle");
if(_uiStyle == null) {
int aSize = theList().count();
if(isSingleSelection()) {
if (aSize <= 5) {
_uiStyle = "radio";
}
if ((aSize >= 5) && (aSize < 20)) {
_uiStyle = "popup";
}
if (aSize >= 20) {
_uiStyle = "browser";
}
} else {
if (aSize <= 5) {
_uiStyle = "checkbox";
}
if (aSize > 5) {
_uiStyle = "browser";
}
}
}
}
return _uiStyle;
}
public boolean isMandatory() {
if(_isMandatory == null) {
_isMandatory = booleanValueForBinding("isMandatory") ? Boolean.TRUE : Boolean.FALSE;
}
return _isMandatory.booleanValue();
}
public boolean isCheckBox() {
return uiStyle().equals("checkbox");
}
public boolean isRadio() {
return uiStyle().equals("radio");
}
public boolean isPopup() {
return uiStyle().equals("popup");
}
public boolean isBrowser() {
return uiStyle().equals("browser");
}
public Object theCurrentItem() {
return theCurrentItem;
}
public void setTheCurrentItem(Object aValue) {
theCurrentItem = aValue;
if (hasBinding("item")) {
setValueForBinding(theCurrentItem, "item");
}
}
public abstract NSArray currentValues();
public NSArray theList() {
if (_list==null) {
if(hasBinding("possibleChoices")) {
_list = (NSArray)valueForBinding("possibleChoices");
if(_list != null && _list.lastObject() instanceof EOEnterpriseObject) {
_list = ERXEOControlUtilities.localInstancesOfObjects(editingContext(), _list);
}
}
if(_list == null) {
EODataSource ds = dataSource();
if(ds.editingContext()!=null) {
_list = ds.fetchObjects();
if(ds.editingContext() != editingContext()) {
_list = ERXEOControlUtilities.localInstancesOfObjects(editingContext(), _list);
}
} else {
log.error("EC of datasource is null, possible resubmit: {}", ERXApplication.erxApplication().extraInformationForExceptionInContext(null, context()));
_list = NSArray.EmptyArray;
}
}
NSArray destinationSortKeys = destinationSortKeys();
NSSelector sorting = (sortCaseInsensitive() ? EOSortOrdering.CompareAscending : EOSortOrdering.CompareCaseInsensitiveAscending);
if (destinationSortKeys != null && destinationSortKeys.count() > 0) {
_list = ERXArrayUtilities.sortedArraySortedWithKeys(_list, destinationSortKeys, sorting);
}
if(includeUnmatchedValues()) {
NSArray currentValues = currentValues();
if(currentValues.count() > 0) {
_unmatchedValues = ERXArrayUtilities.arrayMinusArray(currentValues(), _list);
if(_unmatchedValues.count() > 0) {
_unmatchedValues = ERXArrayUtilities.arrayMinusArray(_unmatchedValues, new NSArray(NO_SELECTION_STRING));
if(_unmatchedValues.lastObject() instanceof EOEnterpriseObject) {
_unmatchedValues = ERXEOControlUtilities.localInstancesOfObjects(editingContext(), _unmatchedValues);
}
if (destinationSortKeys != null && destinationSortKeys.count() > 0) {
_unmatchedValues = ERXArrayUtilities.sortedArraySortedWithKeys(_unmatchedValues, destinationSortKeys, sorting);
}
_list = _list.arrayByAddingObjectsFromArray(_unmatchedValues);
}
} else {
_unmatchedValues = NSArray.EmptyArray;
}
}
}
return _list;
}
public Object theCurrentValue() {
// handle the case where it's the - none - string
Object currentValue;
if (theCurrentItem==NO_SELECTION_STRING) {
currentValue = noneString();
} else if (hasBinding("displayString")) {
currentValue = valueForBinding("displayString");
}
else {
currentValue = NSKeyValueCodingAdditions.Utility.valueForKeyPath(theCurrentItem, destinationDisplayKey());
}
if(localizeDisplayKeys() && currentValue != null && theCurrentItem!=NO_SELECTION_STRING) {
currentValue = localizer().localizedStringForKeyWithDefault(currentValue.toString());
}
if(includeUnmatchedValues() && theCurrentItem!=NO_SELECTION_STRING && unmatchedValues().containsObject(theCurrentItem)) {
currentValue = "[" + currentValue + "]";
}
return currentValue;
}
@Override
public void takeValuesFromRequest(WORequest r, WOContext c) {
// we want to pass the validation here for the case where we are creating a new object
// and are given isMandatory=0 on a mandatory relationship to force users to pick one..
super.takeValuesFromRequest(r, c);
if (c.wasFormSubmitted()) {
Object realSource = realSourceObject();
if(realSource instanceof EOEnterpriseObject) {
EOEnterpriseObject localObject = (EOEnterpriseObject)realSource;
String realRelationshipKey = realRelationshipKey();
Object value = localObject.valueForKeyPath(realRelationshipKey);
try {
localObject.validateValueForKey(value, realRelationshipKey);
} catch (NSValidation.ValidationException eov) {
parent().validationFailedWithException(eov, value, relationshipKey());
}
}
}
}
protected Object realSourceObject() {
Object realSourceObject = sourceObject();
//NOTE ak: this check is needed if we are used in a query binding and want to query a keyPath
if(realSourceObject instanceof EOEnterpriseObject) {
String masterKey = relationshipKey();
if(masterKey.indexOf('.') != -1) {
String partialPath = ERXStringUtilities.keyPathWithoutLastProperty(masterKey);
realSourceObject = NSKeyValueCodingAdditions.Utility.valueForKeyPath(realSourceObject, partialPath);
}
}
return realSourceObject;
}
protected String realRelationshipKey() {
if(sourceObject() instanceof EOEnterpriseObject) {
return ERXStringUtilities.lastPropertyKeyInKeyPath(relationshipKey());
}
return relationshipKey();
}
}