package er.extensions.batching;
import com.webobjects.appserver.WODisplayGroup;
import com.webobjects.eoaccess.EODatabaseDataSource;
import com.webobjects.eocontrol.EODataSource;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOKeyValueUnarchiver;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation._NSDelegate;
import er.extensions.appserver.ERXDisplayGroup;
import er.extensions.eof.ERXBatchFetchUtilities;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.eof.ERXEOControlUtilities;
import er.extensions.eof.ERXKey;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.qualifiers.ERXAndQualifier;
/**
* Extends {@link WODisplayGroup} in order to provide real batching. This is
* done by adding database specific code to the select statement from the
* {@link EOFetchSpecification} from the {@link WODisplayGroup}'s
* {@link EODataSource} which <b>must</b> be an {@link EODatabaseDataSource}.
* If used with other datasources, it reverts to the default behaviour.
*
* @author dt first version
* @author ak gross hacks, made functional and usable.
* @param <T> data type of the displaygroup's objects
*/
public class ERXBatchingDisplayGroup<T> extends ERXDisplayGroup<T> {
/**
* 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;
/** total number of batches */
protected int _batchCount;
/** cache for the displayed objects */
protected NSArray<T> _displayedObjects;
/** cache batching flag */
protected Boolean _isBatching;
protected NSArray<String> _prefetchingRelationshipKeyPaths;
protected int _rowCount = -1;
private boolean _rawRowsForCustomQueries = true;
protected boolean _shouldRememberRowCount = true;
/**
* Creates a new ERXBatchingDisplayGroup.
*/
public ERXBatchingDisplayGroup() {
}
/**
* Sets whether or not fetch specification with custom queries should use raw rows. Defaults to true for backwards compatibility.
*
* @param rawRowsForCustomQueries whether or not fetch specification with custom queries should use raw rows
*/
public void setRawRowsForCustomQueries(boolean rawRowsForCustomQueries) {
_rawRowsForCustomQueries = rawRowsForCustomQueries;
}
/**
* Returns whether or not fetch specification with custom queries should use raw rows.
*
* @return whether or not fetch specification with custom queries should use raw rows
*/
public boolean isRawRowsForCustomQueries() {
return _rawRowsForCustomQueries;
}
/**
* Decodes an ERXBatchingDisplayGroup from the given unarchiver.
*
* @param unarchiver the unarchiver to construct this display group with
* @return the corresponding batching display group
*/
public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver unarchiver) {
return new ERXBatchingDisplayGroup<Object>(unarchiver);
}
/**
* Creates a new ERXBatchingDisplayGroup from an unarchiver.
*
* @param unarchiver the unarchiver to construct this display group with
*/
@SuppressWarnings("unchecked")
private ERXBatchingDisplayGroup(EOKeyValueUnarchiver unarchiver) {
this();
setCurrentBatchIndex(1);
setNumberOfObjectsPerBatch(unarchiver.decodeIntForKey("numberOfObjectsPerBatch"));
setFetchesOnLoad(unarchiver.decodeBoolForKey("fetchesOnLoad"));
setValidatesChangesImmediately(unarchiver.decodeBoolForKey("validatesChangesImmediately"));
setSelectsFirstObjectAfterFetch(unarchiver.decodeBoolForKey("selectsFirstObjectAfterFetch"));
setLocalKeys((NSArray) unarchiver.decodeObjectForKey("localKeys"));
setDataSource((EODataSource) unarchiver.decodeObjectForKey("dataSource"));
setSortOrderings((NSArray) unarchiver.decodeObjectForKey("sortOrdering"));
setQualifier((EOQualifier) unarchiver.decodeObjectForKey("qualifier"));
setDefaultStringMatchFormat((String) unarchiver.decodeObjectForKey("formatForLikeQualifier"));
NSDictionary insertedObjectDefaultValues = (NSDictionary) unarchiver.decodeObjectForKey("insertedObjectDefaultValues");
if (insertedObjectDefaultValues == null) {
insertedObjectDefaultValues = NSDictionary.EmptyDictionary;
}
setInsertedObjectDefaultValues(insertedObjectDefaultValues);
finishInitialization();
}
/**
* If we're batching and the displayed objects have not been fetched,
* do a refetch() of them.
*/
protected void refetchIfNecessary() {
if (isBatching() && _displayedObjects == null) {
refetch();
}
}
/**
* Determines if batching is possible.
*
* @return <code>true</code> if dataSource is an instance of EODatabaseDataSource
*/
protected boolean isBatching() {
return _isBatching == null ? false : _isBatching.booleanValue();
}
/**
* Overridden to set the isBatching flag to <code>true</code> if we have an
* EODatabaseDataSource.
*
* @param datasource the EODataSource to use
*/
@Override
public void setDataSource(EODataSource datasource) {
_isBatching = (datasource instanceof EODatabaseDataSource) ? Boolean.TRUE : Boolean.FALSE;
setRowCount(-1);
super.setDataSource(datasource);
}
/**
* Overridden to return the pre-calculated number of batches.
*
* @return the number of batches to display
*/
@Override
public int batchCount() {
if (isBatching()) {
if (_displayedObjects == null) {
updateBatchCount();
}
return _batchCount;
}
return super.batchCount();
}
/**
* Overridden to clear out our array of fetched objects.
*
* @param index the index of the batch to display
*/
@Override
public void setCurrentBatchIndex(int index) {
int previousBatchIndex = currentBatchIndex();
super.setCurrentBatchIndex(index);
if (isBatching() && previousBatchIndex != index) {
_displayedObjects = null;
}
}
/**
* Overridden to clear out our array of fetched objects.
*
* @param count the maximum number of objects to display per batch
*/
@Override
public void setNumberOfObjectsPerBatch(int count) {
boolean didFetch = _displayedObjects != null;
if (isBatching() && numberOfObjectsPerBatch() != count) {
_displayedObjects = null;
}
NSArray<T> selectedObjects = selectedObjects();
super.setNumberOfObjectsPerBatch(count);
setSelectedObjects(selectedObjects);
// we have already fetched, so we need to adapt the batch count
if (didFetch) {
updateBatchCount();
}
}
/**
* Overridden method in order to fetch -only- the rows that are needed. This
* is different to the editors methods because a {@link WODisplayGroup}
* would always fetch from the start until the end of the objects from the
* fetch limit.
*
* @return the objects that should be displayed.
*/
@Override
public NSArray<T> displayedObjects() {
if (isBatching()) {
refetchIfNecessary();
return _displayedObjects;
}
NSArray<T> displayedObjects = super.displayedObjects();
if (_prefetchingRelationshipKeyPaths != null) {
ERXBatchFetchUtilities.batchFetch(displayedObjects, _prefetchingRelationshipKeyPaths, true);
}
return displayedObjects;
}
/**
* Overridden to refetchIfNecessary() first to ensure we get a correct result in cases
* where this is called before displayedObjects().
*
* @return <code>true</code> if batchCount is greater than 1, <code>false</code> otherwise
*/
@Override
public boolean hasMultipleBatches() {
refetchIfNecessary();
return super.hasMultipleBatches();
}
/**
* Overridden to return allObjects() when batching, as we can't qualify in memory.
*
* @return filtered objects
*/
@Override
public NSArray<T> filteredObjects() {
if (isBatching()) {
return allObjects();
}
return super.filteredObjects();
}
/**
* Overridden to trigger a refetch.
*
* @param qualifier the qualifier for the display group
*/
@Override
public void setQualifier(EOQualifier qualifier) {
super.setQualifier(qualifier);
_displayedObjects = null;
setRowCount(-1);
}
/**
* Overridden to preserve the selected objects.
*
* @param sortOrderings the proposed EOSortOrdering objects
*/
@Override
public void setSortOrderings(NSArray<EOSortOrdering> sortOrderings) {
NSArray<T> selectedObjects = selectedObjects();
super.setSortOrderings(sortOrderings);
setSelectedObjects(selectedObjects);
if (isBatching()) {
_displayedObjects = null;
}
}
/**
* Sets the prefetching key paths to override those in the underlying fetch spec.
*
* @param prefetchingRelationshipKeyPaths the prefetching key paths to override those in the underlying fetch spec
*/
public void setPrefetchingRelationshipKeyPaths(NSArray<String> prefetchingRelationshipKeyPaths) {
_prefetchingRelationshipKeyPaths = prefetchingRelationshipKeyPaths;
}
/**
* Sets the prefetching key paths to override those in the underlying fetch spec.
*
* @param prefetchingRelationshipKeyPaths the prefetching key paths to override those in the underlying fetch spec
*/
public void setPrefetchingRelationshipKeyPaths(ERXKey<?>... prefetchingRelationshipKeyPaths) {
NSMutableArray<String> keypaths = new NSMutableArray<>();
for (ERXKey<?> key : prefetchingRelationshipKeyPaths) {
keypaths.addObject(key.key());
}
_prefetchingRelationshipKeyPaths = keypaths.immutableClone();
}
/**
* Returns the prefetching key paths overriding those in the underlying fetch spec.
*
* @return the prefetching key paths overriding those in the underlying fetch spec
*/
public NSArray<String> prefetchingRelationshipKeyPaths() {
return _prefetchingRelationshipKeyPaths;
}
/**
* Utility to get the fetch specification from the datasource and the filter
* qualifier.
*
* @return the fetch specification
*/
protected EOFetchSpecification fetchSpecification() {
EODatabaseDataSource ds = (EODatabaseDataSource) dataSource();
EOFetchSpecification spec = (EOFetchSpecification) ds.fetchSpecificationForFetch().clone();
spec.setSortOrderings(ERXArrayUtilities.arrayByAddingObjectsFromArrayWithoutDuplicates(sortOrderings(), spec.sortOrderings()));
EOQualifier dgQualifier = qualifier();
EOQualifier qualifier = spec.qualifier();
if (dgQualifier != null) {
if (qualifier != null) {
qualifier = new ERXAndQualifier(dgQualifier, qualifier);
} else {
qualifier = dgQualifier;
}
spec.setQualifier(qualifier);
}
return spec;
}
/**
* Utility to get at the number of rows when batching.
*
* @return number of total rows
*/
public int rowCount() {
int rowCount = _rowCount;
if (rowCount == -1) {
if (isBatching()) {
rowCount = ERXEOAccessUtilities.rowCountForFetchSpecification(dataSource().editingContext(), fetchSpecification());
} else if (dataSource() != null) {
rowCount = dataSource().fetchObjects().count();
} else if (allObjects() != null) {
rowCount = allObjects().count();
}
if (shouldRememberRowCount()) {
_rowCount = rowCount;
}
}
return rowCount;
}
/**
* Override the number of rows of results (if you can
* provide a better estimate than the default behavior). If you
* guess too low, you will never get more than what you set, but
* if you guess too high, it will adjust. Call with -1 to have the rows
* counted again.
*
* @param rowCount the number of rows of results
*/
public void setRowCount(int rowCount) {
_rowCount = rowCount;
}
/**
* @return <code>true</code> if the rowCount() should be remembered after being determined
* or <code>false</code> if rowCount() should be re-calculated when the batch changes
*/
public boolean shouldRememberRowCount() {
return _shouldRememberRowCount;
}
/**
* Set to <code>true</code> to retain the rowCount() after it is determined once for a particular
* qualifier. Set to <code>false</code> to have rowCount() re-calculated when the batch changes.
* The default is <code>true</code>.
*
* @param shouldRememberRowCount the shouldRememberRowCount to set
*/
public void setShouldRememberRowCount(boolean shouldRememberRowCount) {
_shouldRememberRowCount = shouldRememberRowCount;
}
/**
* Utility to fetch the object in a given range.
*
* @param start start index of the range
* @param end end index of the range
* @return array of object in the given range
*/
protected NSArray<T> objectsInRange(int start, int end) {
EOEditingContext ec = dataSource().editingContext();
EOFetchSpecification spec = (EOFetchSpecification)fetchSpecification().clone();
if (_prefetchingRelationshipKeyPaths != null && _prefetchingRelationshipKeyPaths.count() > 0) {
spec.setPrefetchingRelationshipKeyPaths(_prefetchingRelationshipKeyPaths);
}
NSArray result = ERXEOControlUtilities.objectsInRange(ec, spec, start, end, _rawRowsForCustomQueries);
// WAS: fetch the primary keys, turn them into faults, then batch-fetch all
// the non-resident objects
//NSArray primKeys = ERXEOControlUtilities.primaryKeyValuesInRange(ec, spec, start, end);
//NSArray faults = ERXEOControlUtilities.faultsForRawRowsFromEntity(ec, primKeys, spec.entityName());
//NSArray objects = ERXEOControlUtilities.objectsForFaultWithSortOrderings(ec, faults, sortOrderings());
return result;
}
/**
* Utility that does the actual fetching, if a qualifier() is set, it adds
* it to the dataSource() fetch qualifier.
*/
protected void refetch() {
int rowCount = rowCount();
int start = (currentBatchIndex() - 1) * numberOfObjectsPerBatch();
int end = start + numberOfObjectsPerBatch();
if (numberOfObjectsPerBatch() == 0) {
start = 0;
end = rowCount;
}
if (start > rowCount) {
start = rowCount;
}
if (end > rowCount) {
end = rowCount;
}
if (filteredObjects().count() != rowCount) {
NSArray<T> selectedObjects = selectedObjects();
setObjectArray(new FakeArray(rowCount));
setSelectedObjects(selectedObjects);
}
_displayedObjects = objectsInRange(start, end);
// MS: Adjust our guess of the row count if it was
// too high.
if (_rowCount != -1) {
int displayedObjectsCount = _displayedObjects.count();
if (displayedObjectsCount < numberOfObjectsPerBatch()) {
_rowCount = start + _displayedObjects.count();
}
}
}
protected void updateBatchCount() {
if (numberOfObjectsPerBatch() == 0) {
_batchCount = 0;
}
else if (allObjects().count() == 0) {
_batchCount = 1;
}
else {
_batchCount = (rowCount() - 1) / numberOfObjectsPerBatch() + 1;
}
}
/**
* Overridden to update the batch count.
*
* @param objects the object array to set
*/
@Override
@SuppressWarnings("unchecked")
public void setObjectArray(NSArray objects) {
super.setObjectArray(objects);
updateBatchCount();
}
/**
* Overridden to fetch only within displayed limits.
*
* @return <code>null</code> to force the page to reload
*/
@Override
public Object fetch() {
if (isBatching()) {
_NSDelegate delegate = null;
if (delegate() != null) {
delegate = new _NSDelegate(WODisplayGroup.Delegate.class, delegate());
if (delegate.respondsTo("displayGroupShouldFetch") && !delegate.booleanPerform("displayGroupShouldFetch", this)) {
return null;
}
}
if (undoManager() != null) {
undoManager().removeAllActionsWithTarget(this);
}
NSNotificationCenter.defaultCenter().postNotification("WODisplayGroupWillFetch", this);
refetch();
if (delegate != null) {
// was initialized above
if (delegate.respondsTo("displayGroupDidFetchObjects")) {
delegate.perform("displayGroupDidFetchObjects", this, _displayedObjects);
}
}
return null;
}
return super.fetch();
}
@Override
public void updateDisplayedObjects() {
if (isBatching()) {
// refetch();
NSMutableArray<T> selectedObjects = (NSMutableArray<T>) selectedObjects();
NSArray<T> obj = allObjects();
if (delegate() != null) {
_NSDelegate delegate = new _NSDelegate(WODisplayGroup.Delegate.class, delegate());
if (delegate.respondsTo("displayGroupDisplayArrayForObjects")) {
delegate.perform("displayGroupDisplayArrayForObjects", this, obj);
}
}
// _displayedObjects = new NSMutableArray(obj);
setSelectedObjects(selectedObjects);
// selectObjectsIdenticalToSelectFirstOnNoMatch(selectedObjects,
// false);
willChange();
} else {
super.updateDisplayedObjects();
}
}
/**
* Selects the visible objects, overridden to fetch all objects. Note that
* this makes sense only when there are only a "few" objects in the list.
*
* @return <code>null</code> to force the page to reload
*/
@Override
public Object selectFilteredObjects() {
if (isBatching()) {
setSelectedObjects(objectsInRange(0, rowCount()));
return null;
}
return super.selectFilteredObjects();
}
/**
* Dummy array class that is used to provide a certain number of entries. We
* just fake that we an array with the number of objects the display group
* should display.
*/
public static class FakeArray extends NSMutableArray<Object> {
/**
* 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 FakeArray(int count) {
super(count);
Object fakeObject = new NSKeyValueCoding.ErrorHandling() {
public Object handleQueryWithUnboundKey(String anS) {
return null;
}
public void handleTakeValueForUnboundKey(Object anObj, String anS) {
}
public void unableToSetNullForKey(String anS) {
}
};
for (int i = 0; i < count; i++) {
// GROSS HACK: (ak) WO wants to sort the given array via KVC so
// we just let it sort "nothing" objects
insertObjectAtIndex(fakeObject, i);
}
}
}
}