/* * 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.util.Enumeration; import org.apache.log4j.Logger; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WODisplayGroup; import com.webobjects.appserver.WORequest; import com.webobjects.directtoweb.D2WContext; import com.webobjects.directtoweb.ERD2WContext; import com.webobjects.eoaccess.EOGeneralAdaptorException; import com.webobjects.eocontrol.EOArrayDataSource; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOGenericRecord; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSValidation; import com.webobjects.foundation._NSDictionaryUtilities; import er.directtoweb.components.buttons.ERDMassModifyButton; import er.directtoweb.interfaces.ERDObjectSaverInterface; import er.extensions.eof.ERXConstant; import er.extensions.eof.ERXEC; import er.extensions.eof.ERXEOAccessUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; import er.extensions.validation.ERXExceptionHolder; import er.extensions.validation.ERXValidation; /** * List page for editing all items in the list. * Name your page EditListYourEntityName. task will be edit, subTask will be list. * See Component ERD2WEditableListTemplate for html/wod example. * * There is a "mass change" feature that can apply a change to all displayed objects. * Think of it as an "input assistant". The changes are not saved when propagated, and the rows can be updated individually after a mass change has been applied. * (Note: There is a {@link ERDMassModifyButton} class that may be more appropriate depending on your needs) * * To enable the mass change feature on an editable list page, do the following: * * 1/ Add a "showMassChange" rule that returns "true" for your edit list page * 2/ If you want to restrict the keys that can be "mass edited", add a displayPropertyKeys rule with a restricted set of keys with the qualifer "(massChangeEntityDisplay = 1)" * * Known Issues: * changing the number of items per batch causes problems (the display group's batch is updated too soon in the request/response loop) * @d2wKey showBanner * @d2wKey object * @d2wKey isEntityInspectable * @d2wKey shouldValidateBeforeSave * @d2wKey shouldSaveChanges * @d2wKey shouldRecoverFromOptimisticLockingFailure * @d2wKey saveLabelTemplateKey * @d2wKey displayNameForEntity * @d2wKey showMassChange */ public class ERD2WEditableListPage extends ERD2WListPage implements ERXExceptionHolder, ERDObjectSaverInterface { /** * 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 final Logger log = Logger.getLogger(ERD2WEditableListPage.class); public ERD2WEditableListPage(WOContext context) {super(context);} public int colspanForNavBar() { return 2*displayPropertyKeys().count()+2; } @Override public int numberOfObjectsPerBatch() { // if we are not showing the nav bar, do not batch the display group (since user will have no way to navigate batches) if (!ERXValueUtilities.booleanValueWithDefault(d2wContext().valueForKey("showBanner"), true)) { return 0; } else { return super.numberOfObjectsPerBatch(); } } /* FIXME: 1/ we are missing a formal protocol for selectedObject 2/ this component uses a hidden trick: - clicking on the select button uses next page delegate - the cancel button uses the next page */ private boolean _objectWasSaved; public boolean objectWasSaved() { return _objectWasSaved; } private NSMutableDictionary _errorMessagesDictionaries; protected NSMutableDictionary errorMessagesDictionaries(){ if(_errorMessagesDictionaries == null){ _errorMessagesDictionaries = new NSMutableDictionary(); } return _errorMessagesDictionaries; } public NSMutableDictionary errorDictionaryForObject(Object object) { int hashCode = object != null ? object.hashCode() : 0; Object key = ERXConstant.integerForInt(hashCode); if (errorMessagesDictionaries().objectForKey(key) == null) { errorMessagesDictionaries().setObjectForKey(new NSMutableDictionary(), key); } if (log.isDebugEnabled()) log.debug("errorDictionaryForObject("+object+") hashCode/key: "+hashCode+"/"+key+" errorMessages: "+errorMessagesDictionaries().objectForKey(key)); return (NSMutableDictionary)errorMessagesDictionaries().objectForKey(key); } public NSMutableDictionary currentErrorDictionary() { Object key = d2wContext().valueForKeyPath("object.hashCode"); if (errorMessagesDictionaries().objectForKey(key) == null) { errorMessagesDictionaries().setObjectForKey(new NSMutableDictionary(), key); } if (log.isDebugEnabled()) log.debug("currentErrorDictionary() key: "+key+" errorMessages: "+errorMessagesDictionaries().objectForKey(key)); return (NSMutableDictionary)errorMessagesDictionaries().objectForKey(key); } public String dummy; @Override public boolean showCancel() { return _nextPage!=null; } @Override public boolean isEntityInspectable() { return isEntityReadOnly() && ERXValueUtilities.booleanValue(d2wContext().valueForKey("isEntityInspectable")); } @Override public void setObject(EOEnterpriseObject eo) { super.setObject(eo); } @Override public WOComponent backAction() { return super.backAction(); } @Override public WOComponent nextPage() { return (nextPageDelegate() != null) ? nextPageDelegate().nextPage(this) : super.nextPage(); } public boolean shouldValidateBeforeSave() { return ERXValueUtilities.booleanValue(d2wContext().valueForKey("shouldValidateBeforeSave")); } public boolean shouldSaveChanges() { return ERXValueUtilities.booleanValue(d2wContext().valueForKey("shouldSaveChanges")); } public boolean shouldRecoverFromOptimisticLockingFailure() { return ERXValueUtilities.booleanValueWithDefault(d2wContext().valueForKey("shouldRecoverFromOptimisticLockingFailure"), false); } private static final NSSelector ValidateForInsertSelector = new NSSelector("validateForInsert"); private static final NSSelector ValidateForSaveSelector = new NSSelector("validateForUpdate"); public boolean tryToSaveChanges(boolean validateObjects) { if (log.isDebugEnabled()) log.debug("tryToSaveChanges() validateObjects: "+validateObjects+" shouldSaveChanges: "+shouldSaveChanges()); boolean saved = false; try { if (!isListEmpty() && validateObjects && shouldValidateBeforeSave()) { if (log.isDebugEnabled()) log.debug("tryToSaveChanges calling validateForSave"); editingContext().insertedObjects().makeObjectsPerformSelector(ValidateForInsertSelector, (Object[])null); editingContext().updatedObjects().makeObjectsPerformSelector(ValidateForSaveSelector, (Object[])null); } if (!isListEmpty() && shouldSaveChanges() && editingContext().hasChanges()) editingContext().saveChanges(); saved = true; } catch (NSValidation.ValidationException ex) { setErrorMessage(ERXLocalizer.currentLocalizer().localizedTemplateStringForKeyWithObject("CouldNotSave", ex)); validationFailedWithException(ex, ex.object(), "saveChangesExceptionKey"); } catch(EOGeneralAdaptorException ex) { if(shouldRecoverFromOptimisticLockingFailure()) { EOEnterpriseObject eo = ERXEOAccessUtilities.refetchFailedObject(editingContext(), ex); setErrorMessage(ERXLocalizer.currentLocalizer().localizedTemplateStringForKeyWithObject("CouldNotSavePleaseReapply", d2wContext())); validationFailedWithException(ex, eo, "CouldNotSavePleaseReapply"); } else { throw ex; } } return saved; } public WOComponent saveAction() { WOComponent returnComponent = null; if (errorMessages.count()==0) { try { _objectWasSaved=true; returnComponent = tryToSaveChanges(true) ? nextPage() : null; } finally { _objectWasSaved=false; } } else { // if we don't do this, we end up with the error message in two places // in errorMessages and errorMessage (super class) errorMessage=null; } return returnComponent; } public WOComponent cancel(){ clearValidationFailed(); if(!isListEmpty()) { editingContext().revert(); } if(_massChangeEO != null) { if(_massChangeEO.editingContext() != null) { _massChangeEO.editingContext().revert(); } _massChangeEO = null; _massChangeDisplayGroup = null; } return backAction(); } @Override public void validationFailedWithException (Throwable e, Object value, String keyPath) { if (value != null) { ERXValidation.validationFailedWithException(e, value, keyPath, errorDictionaryForObject(value), propertyKey(), ERXLocalizer.currentLocalizer(), d2wContext().entity(), shouldSetFailedValidationValue()); } super.validationFailedWithException(e, value, keyPath); } @Override public void clearValidationFailed(){ super.clearValidationFailed(); setErrorMessage(null); for(Enumeration e = errorMessagesDictionaries().objectEnumerator(); e.hasMoreElements();){ ((NSMutableDictionary)e.nextElement()).removeAllObjects(); } } public WOComponent update() { tryToSaveChanges(true); return null; } @Override public void takeValuesFromRequest(WORequest r, WOContext c) { // Need to make sure that we have a clean slate, every time clearValidationFailed(); _errorMessagesDictionaries = null; super.takeValuesFromRequest(r, c); } public String saveLabel() { String templateKey = (String)d2wContext().valueForKey("saveLabelTemplateKey"); String displayName = (String)d2wContext().valueForKey("displayNameForEntity"); int count = displayGroup().allObjects().count(); if(templateKey == null) templateKey = "ERDEditList.saveLabel"; String saveLabel = ERXLocalizer.currentLocalizer().plurifiedStringWithTemplateForKey(templateKey, displayName, count, d2wContext()); if (log.isDebugEnabled()) log.debug("saveLabel() - "+saveLabel); return saveLabel; } // // Mass change feature // public boolean shouldShowMassChange() { int displayedObjectsCount = displayGroup().displayedObjects() != null ? displayGroup().displayedObjects().count() : 0; return displayedObjectsCount > 0 && ERXValueUtilities.booleanValue(d2wContext().valueForKey("showMassChange")); } public static final String MassChangeEntityDisplayKey = "massChangeEntityDisplay"; private D2WContext _d2wContextForMassChangeEO; public D2WContext d2wContextForMassChangeEO() { if (_d2wContextForMassChangeEO == null) { _d2wContextForMassChangeEO = ERD2WContext.newContext(d2wContext()); _d2wContextForMassChangeEO.takeValueForKey(Boolean.TRUE, MassChangeEntityDisplayKey); } return _d2wContextForMassChangeEO; } protected WODisplayGroup _massChangeDisplayGroup; public WODisplayGroup massChangeDisplayGroup() { if (_massChangeDisplayGroup == null) { final EOArrayDataSource ads = new EOArrayDataSource(massChangeEO().classDescription(), massChangeEO().editingContext()); ads.setArray(new NSArray(massChangeEO())); _massChangeDisplayGroup = new WODisplayGroup(); _massChangeDisplayGroup.setDataSource(ads); _massChangeDisplayGroup.setNumberOfObjectsPerBatch(0); _massChangeDisplayGroup.fetch(); if(log.isDebugEnabled()) log.debug("_massChangeDisplayGroup: " + _massChangeDisplayGroup); } return _massChangeDisplayGroup; } // custom generic record class that manages unbound keys in a dictionary. public class ERDMassChangeGenericRecord extends EOGenericRecord { /** * 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; // dictionary of non property key values private NSMutableDictionary _unboundKeyDictionary; public ERDMassChangeGenericRecord(EOClassDescription classDescription) { super(classDescription); _unboundKeyDictionary = new NSMutableDictionary(); } @Override public Object handleQueryWithUnboundKey(String key) { return _unboundKeyDictionary.objectForKey(key); } @Override public void handleTakeValueForUnboundKey(Object value, String key) { _unboundKeyDictionary.takeValueForKey(value, key); } public NSDictionary pendingChanges() { final NSMutableDictionary combinedKeyChanges = _unboundKeyDictionary.mutableClone(); final NSDictionary changesFromSnapshot = massChangeEO().changesFromCommittedSnapshot(); if (changesFromSnapshot!= null && changesFromSnapshot.count() > 0) { combinedKeyChanges.addEntriesFromDictionary(changesFromSnapshot); } return combinedKeyChanges.immutableClone(); } public void clearPendingChanges() { final NSDictionary changesFromSnapshot = massChangeEO().changesFromCommittedSnapshot(); if (changesFromSnapshot != null && changesFromSnapshot.count() > 0) { NSDictionary nullValues = _NSDictionaryUtilities.dictionaryWithNullValuesForKeys(changesFromSnapshot.allKeys()); _massChangeEO.takeValuesFromDictionary(nullValues); } _unboundKeyDictionary.removeAllObjects(); } // committed snapshot convenience from ERXGenericRecord private NSDictionary changesFromCommittedSnapshot() { return changesFromSnapshot(editingContext().committedSnapshotForObject(this)); } } protected ERDMassChangeGenericRecord _massChangeEO; public ERDMassChangeGenericRecord massChangeEO() { if (_massChangeEO == null) { // create our dummy EO to hold our potential mass changes _massChangeEO = new ERDMassChangeGenericRecord(EOClassDescription.classDescriptionForEntityName(d2wContext().entity().name())); ERXEC.newEditingContext().insertObject(_massChangeEO); if(log.isDebugEnabled()) log.debug("Created _massChangeEO (for entity: "+d2wContext().entity().name()+"): " + _massChangeEO); } return _massChangeEO; } public WOComponent clearMassChangeEO() { if (_massChangeEO != null) { _massChangeEO.clearPendingChanges(); } return null; } private static final NSSelector TakeValuesFromDictionarySelector = new NSSelector("takeValuesFromDictionary", new Class [] {NSDictionary.class}); private NSDictionary _lastAppliedMassChanges; public WOComponent propagateChangesToVisibleObjects() { final NSArray displayedObjects = displayGroup().displayedObjects(); final NSDictionary changes = massChangeEO().pendingChanges(); _lastAppliedMassChanges = null; if (displayedObjects != null && changes != null && changes.count() > 0) { displayedObjects.makeObjectsPerformSelector(TakeValuesFromDictionarySelector, new Object[]{changes}); _lastAppliedMassChanges = changes; clearMassChangeEO(); } return null; } public String propagateChangesDetails() { return "last applied changes: " + _lastAppliedMassChanges + "<br>current changes: " + massChangeEO().pendingChanges(); } }