package er.extensions.appserver; import java.lang.reflect.Field; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WODisplayGroup; import com.webobjects.eoaccess.EODatabaseDataSource; import com.webobjects.eocontrol.EOAndQualifier; import com.webobjects.eocontrol.EODataSource; 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.NSForwardException; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSSet; import com.webobjects.foundation._NSArrayUtilities; import er.extensions.batching.ERXBatchingDisplayGroup; import er.extensions.eof.ERXEOAccessUtilities; import er.extensions.eof.ERXS; /** * Extends {@link WODisplayGroup} * <ul> * <li>provide access to the filtered objects</li> * <li>allows you to add qualifiers to the final query qualifier (as opposed to just min/equals/max with the keys)</li> * <li>clears out the sort ordering when the datasource changes. This is a cure fix to prevent errors when using switch components.</li> * </ul> * <h2>Selections and usage with Datasource</h2> * If you are using the display group with a datasource and changing the selection by {@link #setSelectedObjects(NSArray)} * only matching objects in the displayed objects will be selected and the events <i>displayGroupShouldChangeSelectionToIndexes</i>, * <i>displayGroupDidChangeSelectedObjects</i> and <i>displayGroupDidChangeSelection</i> will be triggered. * <h2>Selections and usage without Datasource</h2> * If you are using plain arrays to fill your display group and set a selection by {@link #setSelectedObjects(NSArray)} you won't * get related events as with a datasource. Also the selection is not matched against the displayed objects but set directly. * * @author ak * @param <T> data type of the displaygroup's objects */ public class ERXDisplayGroup<T> extends WODisplayGroup { /** * {@code _displayedObjects} field in parent object */ private transient Field displayedObjectsField; /** * cache filtered objects to avoid repeated qualification */ private transient NSArray<T> _filteredObjects; /** * 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; private static final Logger log = LoggerFactory.getLogger(ERXDisplayGroup.class); public ERXDisplayGroup() { super(); } /** * Fetches the {@code _displayedObjects} field from the parent * {@link WODisplayGroup}. This is used in the overriden * {@link #setSelectedObjects(NSArray)} method. * * @return {@code _displayedObjects} field from parent object */ private Field displayedObjectsField() { if (displayedObjectsField == null) { try { displayedObjectsField = WODisplayGroup.class.getDeclaredField("_displayedObjects"); displayedObjectsField.setAccessible(true); } catch (SecurityException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (NoSuchFieldException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } return displayedObjectsField; } /** * Decodes an ERXDisplayGroup 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 ERXDisplayGroup<Object>(unarchiver); } /** * Creates a new ERXBatchingDisplayGroup from an unarchiver. * * @param unarchiver the unarchiver to construct this display group with */ @SuppressWarnings("unchecked") private ERXDisplayGroup(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(); } /** * Holds the extra qualifiers. */ private NSMutableDictionary<String, EOQualifier> _extraQualifiers = new NSMutableDictionary<>(); public void setQualifierForKey(EOQualifier qualifier, String key) { if(qualifier != null) { _extraQualifiers.setObjectForKey(qualifier, key); } else { _extraQualifiers.removeObjectForKey(key); } } /** * Will return the qualifier set by "setQualifierForKey()" if it exists. Null returns otherwise. * @param key * @return */ public EOQualifier qualifierForKey(String key) { EOQualifier qualifier = null; if (StringUtils.isNotBlank(key)) { qualifier = _extraQualifiers.objectForKey(key); } return qualifier; } /** * Overridden to support extra qualifiers. * @return the qualifier constructed */ @Override public EOQualifier qualifierFromQueryValues() { EOQualifier q1 = super.qualifierFromQueryValues(); EOQualifier q2 = null; if(_extraQualifiers.allValues().count() > 1) { q2 = new EOAndQualifier(_extraQualifiers.allValues()); } else if(_extraQualifiers.allValues().count() > 0) { q2 = _extraQualifiers.allValues().lastObject(); } return q1 == null ? q2 : (q2 == null ? q1 : new EOAndQualifier(new NSArray<EOQualifier>(new EOQualifier[] {q1, q2}))); } /** * Overridden to localize the fetch specification if needed. * @return <code>null</code> to force the page to reload */ @Override public Object fetch() { if(log.isDebugEnabled()) { log.debug("Fetching: {}", this, new RuntimeException("Dummy for Stacktrace")); } Object result; // ak: we need to transform localized keys (foo.name->foo.name_de) // when we do a real fetch. This actually // belongs into ERXEC, but I'm reluctant to have this morphing done // every time a fetch occurs as it affects mainly sort ordering // from the display group if (dataSource() instanceof EODatabaseDataSource) { EODatabaseDataSource ds = (EODatabaseDataSource) dataSource(); EOFetchSpecification old = ds.fetchSpecification(); EOFetchSpecification fs = ERXEOAccessUtilities.localizeFetchSpecification(ds.editingContext(), old); ds.setFetchSpecification(fs); try { result = super.fetch(); } finally { ds.setFetchSpecification(old); } } else { result = super.fetch(); } // flush cache _filteredObjects = null; return result; } @Override public void setQualifier(EOQualifier qualifier) { super.setQualifier(qualifier); // flush cache _filteredObjects = null; } /** * Returns all objects, filtered by the qualifier(). * @return filtered objects */ public NSArray<T> filteredObjects() { if (qualifier() == null) { return allObjects(); } else if (_filteredObjects == null) { _filteredObjects = EOQualifier.filteredArrayWithQualifier(allObjects(), qualifier()); } return _filteredObjects; } /** * Returns allObjects(), first filtered by the qualifier(), then sorted by the sortOrderings(). * @return sorted filtered objects */ public NSArray<T> sortedObjects() { return ERXS.sorted(filteredObjects(), sortOrderings()); } @Override public NSArray<T> selectedObjects() { if(log.isDebugEnabled()) { log.debug("selectedObjects@{}:{}", hashCode(), super.selectedObjects().count()); } return super.selectedObjects(); } @Override public void setSelectedObjects(NSArray objects) { if(log.isDebugEnabled()) { log.debug("setSelectedObjects@{}:{}", hashCode(), (objects != null ? objects.count() : "0")); } if (this instanceof ERXBatchingDisplayGroup || dataSource() == null) { // keep previous behavior // CHECKME a batching display group has its own _displayedObjects variable so setSelectionIndexes won't work super.setSelectedObjects(objects); } else { // jw: don't call super as it does not call setSelectionIndexes as advertised in its // javadocs and thus doesn't invoke events on the delegate // we need to access the private field _displayedObjects directly as we would get // wrong indexes when calling displayedObjects() NSMutableArray displayedObjects; try { displayedObjects = (NSMutableArray) displayedObjectsField().get(this); } catch (IllegalArgumentException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (IllegalAccessException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } NSArray<Integer> newSelection = _NSArrayUtilities.indexesForObjectsIndenticalTo(displayedObjects, objects); setSelectionIndexes(newSelection); } } @Override public boolean setSelectionIndexes(NSArray nsarray) { if(log.isDebugEnabled()) { log.debug("setSelectionIndexes@{}:{}", hashCode(), (nsarray != null ? nsarray.count() : "0"), new RuntimeException("Dummy for Stacktrace")); } return super.setSelectionIndexes(nsarray); } /** * Extends the current selection by the given object. * @param object object to add to the selection * @return <code>true</code> if the object was added or <code>false</code> otherwise */ public boolean addToSelection(T object) { if (object == null) { return false; } return addToSelection(new NSArray<T>(object)); } /** * Extends the current selection by the given objects. * @param objects objects to add to the selection * @return <code>true</code> if at least one object was added or <code>false</code> otherwise */ public boolean addToSelection(NSArray<T> objects) { if (objects == null || objects.isEmpty()) { return false; } NSMutableSet<T> selection = new NSMutableSet<>(selectedObjects()); int selectionCountBefore = selection.count(); selection.addObjectsFromArray(objects); setSelectedObjects(selection.allObjects()); return selection.count() != selectionCountBefore; } /** * Removes the given object from the current selection. * @param object object to remove from the selection * @return <code>true</code> if the object was removed or <code>false</code> otherwise */ public boolean removeFromSelection(T object) { if (object == null) { return false; } return removeFromSelection(new NSArray<T>(object)); } /** * Removes the given objects from the current selection. * @param objects objects to remove from the selection * @return <code>true</code> if at least one object was removed or <code>false</code> otherwise */ public boolean removeFromSelection(NSArray<T> objects) { if (objects == null || objects.isEmpty()) { return false; } NSMutableSet<T> selection = new NSMutableSet<>(selectedObjects()); int selectionCountBefore = selection.count(); NSSet<T> objectsToRemove = new NSSet<>(objects); selection.subtractSet(objectsToRemove); setSelectedObjects(selection.allObjects()); return selection.count() != selectionCountBefore; } /** * Overridden to preserve the current selection. * @param count the proposed number of objects the WODisplayGroup should display at a time */ @Override public void setNumberOfObjectsPerBatch(int count) { NSArray<T> oldSelection = selectedObjects(); super.setNumberOfObjectsPerBatch(count); setSelectedObjects(oldSelection); } /** * Overridden to clear out the sort ordering if it is no longer applicable. * @param ds the proposed EODataSource */ @Override public void setDataSource(EODataSource ds) { EODataSource old = dataSource(); super.setDataSource(ds); if(old != null && ds != null && ObjectUtils.notEqual(old.classDescriptionForObjects(), ds.classDescriptionForObjects())) { setSortOrderings(NSArray.EmptyArray); } } /** * Overridden to preserve the current selection. * @return <code>null</code> to force the page to reload */ @Override public Object displayNextBatch() { NSArray<T> oldSelection = selectedObjects(); Object result = super.displayNextBatch(); setSelectedObjects(oldSelection); return result; } /** * Overridden to preserve the current selection. * @return <code>null</code> to force the page to reload */ @Override public Object displayPreviousBatch() { NSArray<T> oldSelection = selectedObjects(); Object result = super.displayPreviousBatch(); setSelectedObjects(oldSelection); return result; } /** * Selects the visible objects. * @return <code>null</code> to force the page to reload */ public Object selectFilteredObjects() { setSelectedObjects(filteredObjects()); return null; } /** * Overridden to log a message when more than one sort order exists. Useful to track down errors. * @param sortOrderings the proposed EOSortOrdering objects */ @Override public void setSortOrderings(NSArray<EOSortOrdering> sortOrderings) { super.setSortOrderings(sortOrderings); if (sortOrderings != null && sortOrderings.count() > 1) { log.debug("More than one sort order: {}", sortOrderings); } } public void clearExtraQualifiers() { _extraQualifiers.removeAllObjects(); } /* Generified methods */ @Override public NSArray<T> allObjects() { return super.allObjects(); } @Override public NSArray<String> allQualifierOperators() { return super.allQualifierOperators(); } @Override public NSArray<T> displayedObjects() { return super.displayedObjects(); } @Override public T selectedObject() { return (T) super.selectedObject(); } @Override public NSArray<EOSortOrdering> sortOrderings() { return super.sortOrderings(); } /** * Overridden to return correct result when no objects are displayed * @return the index of the first object displayed by the current batch */ @Override public int indexOfFirstDisplayedObject() { if (currentBatchIndex() == 1 && displayedObjects().count() == 0) return 0; return super.indexOfFirstDisplayedObject(); } /** * Overridden to return correct index if the number of filtered objects * is not a multiple of <code>numberOfObjectsPerBatch</code> and we are * on the last batch index. The superclass incorrectly uses allObjects * instead of displayedObjects to determine the index value. * @return the index of the last object displayed by the current batch */ @Override public int indexOfLastDisplayedObject() { int computedEnd = numberOfObjectsPerBatch() * currentBatchIndex(); int realEnd = displayedObjects().count(); if(numberOfObjectsPerBatch() == 0) { return realEnd; } if (currentBatchIndex() > 1) { realEnd += numberOfObjectsPerBatch() * (currentBatchIndex() - 1); } return realEnd >= computedEnd ? computedEnd : realEnd; } }