/* * Copyright (C) Quetzal Consulting, Inc. All rights reserved. * * This class was originally developed by Robert A. Decker * at Quetzal Consulting * * 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.extensions.components.javascript; import java.util.Enumeration; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableArray; import er.extensions.components.ERXStatelessComponent; import er.extensions.components._private.ERXWOForm; import er.extensions.foundation.ERXArrayUtilities; /** * Very, very cool js component. Implements master-detail with js in two popups, ie the first popup could be say * states and depending on which state is picked the second popup might reflect all of the cities of that state. * <p> * This WOComponent displays two pop-up buttons. One pop-up displays a list of what can be thought of as parent entities. * The second pop-up displays a list of what can be thought of as children entities. When a user selects an entity in * the parent list, the child list is instantly modified to reflect the children entities available to the user * through that parent. This is done through client-side Javascript. Also handles to-many selections both on the * parent and the children. * <p> * For example: <pre><code> parent1(child1,child2,child3) parent2(child4,child5) parent3(child2,child5) </code></pre> * When the user selects parent1, its appropriate children are displayed in the second popup. * If the user selects child2 in the children pop-up this is the value that is returned to the * user through the childrenSelection variable. This is either an NSArray if <code>multiple</code> is true * or the single selected object. * For the display of the parent popup, if we aren't passed in a parentSelection, then we default to * parentPopUpStringForAll. If we aren't given that either, then we default to the first parent in the array. * For the display of the child popup, if we aren't passed in a childrenSelection, then we default to childPopUpStringForAll. * If we aren't given that either, then we default to the first child in the array. * * @binding multiple boolean the defines if there can multiple parents and children selected. * @binding parentEntitiesList array of the parent objects that appear in the first pop-up. * @binding parentToChildrenRelationshipName name of the relationship from the parent to its possible children. This is used to fill the values that appear in the children popup. * @binding parentSelection currently selected parent(s) in the parent pop-up. This can be null, but will return the user-selected parent. * @binding childrenSelection returns the user-selected child(ren). * @binding parentDisplayValueName keypath of the parent displayed in the parent pop-up * @binding parentLabel value displayed in the table interface for the parent popup. * @binding childLabel value displayed in the table interface for the child popup. * @binding defaultChildKey keypath of the parent for the default child (eg, largest city) * @binding childrenSortKey keypath to sort the children on * @binding childDisplayValueName keypath of the child displayed in the child pop-up * @binding parentPopUpStringForAll to display if no parent is chosen ("- all -") * @binding childPopUpStringForAll to display if no child is chosen ("- all -") * @binding size number of rows in select boxes * @binding possibleChildren shows only these values for children */ public class ERXJSPopUpRelationPicker extends ERXStatelessComponent { /** * 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 ERXJSPopUpRelationPicker(WOContext aContext) { super(aContext); } private final static Logger log = LoggerFactory.getLogger(ERXJSPopUpRelationPicker.class); private final static Logger jsLog = LoggerFactory.getLogger("er.extensions.ERXJSPopUpRelationPicker.script"); protected Integer _size; protected String _childDisplayValueName; protected String _parentDisplayValueName; protected NSArray _parentEntitiesList; protected String _parentToChildrenRelationshipName; protected NSArray _parentSelection; protected NSArray _childrenSelection; protected String _parentPopUpStringForAll; // for example, '-- all --' or '-- none --'. sets selectedParent to null. All children displayed for all parents if picked. protected String _childPopUpStringForAll; // for example, '-- all --' or '-- none --'. sets selectedChild to null protected String _parentLabel; protected String _childLabel; protected String _childrenSortKey; protected String _defaultChildKey; protected Boolean _multiple; protected NSArray _possibleChildren; protected String parentSelectName; protected String childSelectName; protected String pickerName; protected String objectsArrayName; private static final int NOT_FOUND = -1; private static final NSArray UNSET = new NSArray(); @Override public void awake() { super.awake(); updateVarNames(); } protected void updateVarNames() { String elementID = context().elementID(); elementID = StringUtils.replace(elementID, ".", "_"); pickerName = "picker_"+ elementID; parentSelectName = "parent_" + elementID; childSelectName = "child_" + elementID; objectsArrayName = "parents_children_" + elementID; } protected int offsetForID(String id) { if(!(id == null || id.trim().length() == 0 || "WONoSelectionString".equals(id))) { try { return Integer.parseInt(id); } catch (Exception e) { log.info("Exception while parsing ID", e); } } return NOT_FOUND; } protected Object parentFromID(String id) { int offset = offsetForID(id); if(offset != NOT_FOUND) { return parentEntitiesList().objectAtIndex(offset); } return null; } protected Object idForParent(Object parent) { if(parent != null) { return Integer.valueOf(parentEntitiesList().indexOfObject(parent)); } return null; } protected Object childFromID(Object parent, String id) { if(id != null) { NSArray ids = NSArray.componentsSeparatedByString(id, "|"); if(ids.count() == 2) { if(parent == null) { parent = parentFromID((String)ids.objectAtIndex(0)); } int offset = offsetForID((String)ids.objectAtIndex(1)); if(offset != NOT_FOUND && parent != null) { return sortedChildren(parent).objectAtIndex(offset); } } else { log.info("Child ID not valid: {}", id); } } return null; } protected int offsetForChild(Object parent, Object child) { return parent != null ? sortedChildren(parent).indexOfObject(child) : NOT_FOUND; } protected Object idForChild(Object parent, Object child) { if(parent != null) { int offset = sortedChildren(parent).indexOfObject(child); if(offset != NOT_FOUND) { return idForParent(parent) + "|" + offset; } } else { for (Enumeration parents = parentEntitiesList().objectEnumerator(); parents.hasMoreElements();) { Object aParent = parents.nextElement(); int offset = sortedChildren(aParent).indexOfObject(child); if(offset != NOT_FOUND) { return idForParent(aParent) + "|" + offset; } } } return null; } @Override public void takeValuesFromRequest(WORequest request, WOContext context) { NSArray parentFormValues = request.formValuesForKey(parentSelectName); NSArray childFormValues = request.formValuesForKey(childSelectName); if(parentFormValues != null && childFormValues != null) { if(parentFormValues.containsObject("WONoSelectionString")) { setSelectedParents(null); } else { NSMutableArray parents = new NSMutableArray(); for (Enumeration ids = parentFormValues.objectEnumerator(); ids.hasMoreElements();) { Object parent = parentFromID((String) ids.nextElement()); if(parent != null) { parents.addObject(parent); } } setSelectedParents(parents); } if(childFormValues.containsObject("WONoSelectionString")) { NSArray children = (NSArray)parentSelection().valueForKeyPath(parentToChildrenRelationshipName()); if(parentFormValues.containsObject("WONoSelectionString")) { setChildrenSelection(null); } else { if(!multiple()) { setChildrenSelection(null); } else { setChildrenSelection(ERXArrayUtilities.flatten(children)); } } } else { NSMutableArray children = new NSMutableArray(); for (Enumeration ids = childFormValues.objectEnumerator(); ids.hasMoreElements();) { Object child = childFromID(null, (String) ids.nextElement()); if(child != null) { children.addObject(child); } } setChildrenSelection(children); } } super.takeValuesFromRequest(request, context); } protected NSArray possibleChildren() { if(_possibleChildren != null) { _possibleChildren = (NSArray) valueForBinding("possibleChildren"); if(_possibleChildren == null) { _possibleChildren = UNSET; } } return _possibleChildren == UNSET ? null : _possibleChildren; } protected NSArray unsortedChildren(Object parent) { NSArray result = (NSArray)NSKeyValueCodingAdditions.Utility.valueForKeyPath(parent, parentToChildrenRelationshipName() + ((parent instanceof NSArray) ? ".@flatten.@removeNullValues" : "")); NSArray restrictedChoices = possibleChildren(); if(restrictedChoices != null) { result = ERXArrayUtilities.intersectingElements(result, restrictedChoices); } return result; } protected NSArray sortedChildren(Object parent) { EOSortOrdering sortOrdering=new EOSortOrdering(childrenSortKey(), EOSortOrdering.CompareAscending); NSMutableArray sortArray=new NSMutableArray(sortOrdering); NSArray result=EOSortOrdering.sortedArrayUsingKeyOrderArray(unsortedChildren(parent), sortArray); return result!=null ? result : NSArray.EmptyArray; } public String jsString() { // this method returns all the javascript we need to embed in the web page. StringBuilder returnString = new StringBuilder(2000); returnString.append('\n'); // This Javascript string builds an array of Entity objects on the browser end. returnString.append(objectArrayCreationString()); returnString.append('\n'); jsLog.debug("JSPopUpRelationPicker jsString returnString is {}", returnString); return returnString.toString(); } public String hiddenFormElementStrings() { StringBuilder returnString = new StringBuilder(500); returnString.append("\n<script language=\"JavaScript\">"); String childToSelect = ""; if (!multiple() && childrenSelection()!=null && childrenSelection().count()==1 && parentSelection()!=null && parentSelection().count()==1) { int childToSelectInt=offsetForChild(parentSelection().objectAtIndex(0), childrenSelection().objectAtIndex(0)); if (childToSelectInt!=NOT_FOUND) childToSelect=""+childToSelectInt; } returnString.append("\nvar " + pickerName +" = new ERXJSPopupRelationshipPicker(" + objectsArrayName + "," + "window.document." + formName() + "." + parentSelectName + "," + (parentPopUpStringForAll() != null ? "\"" + parentPopUpStringForAll().replace('\n',' ') + "\"" : "null") + "," + "window.document." + formName() + "." + childSelectName + "," + (childPopUpStringForAll() != null ? "\"" + childPopUpStringForAll() + "\"" : "null") +");\n" + (parentPopUpStringForAll() == null ? pickerName + ".parentChanged("+childToSelect+");" : "") +"\n</script>"); log.debug("{}", returnString); // trigger an update of the parent - this causes the child to be properly set to a sub selection (instead of listing all possible value) when // we are editing a new object return returnString.toString(); } public String parentPopUpString() { StringBuffer returnString = selectHeader(parentSelectName, pickerName + ".parentChanged();"); if (parentSelection().count() == 0 && parentPopUpStringForAll()==null) setSelectedParents(new NSArray(parentEntitiesList().objectAtIndex(0))); int iCount = parentEntitiesList().count(); for (int i=0;i<iCount;i++) { Object aEntity = parentEntitiesList().objectAtIndex(i); returnString.append("\t<option "); if (isSelectedParent(aEntity)) { returnString.append("selected=\"selected\" "); } returnString.append("value=\"" + idForParent(aEntity) + "\">"); returnString.append(NSKeyValueCodingAdditions.Utility.valueForKeyPath(aEntity, parentDisplayValueName())); returnString.append("</option>\n"); } returnString.append("</select>\n"); return returnString.toString(); } public String formName() { return ERXWOForm.formName(context(), "forms[0]"); } /** * @return the string to create the pop-up with the initial child values something like: <pre><select name="children_select"> <option value=4>poodle <option selected value=5>puli <option value=6>greyhound </select></pre> */ public String childPopUpString() { StringBuffer returnString = selectHeader(childSelectName, pickerName + ".childChanged();"); if (parentSelection().count() != 0) { if (childrenSelection().count() == 0 && defaultChildKey() != null) setChildrenSelection((NSArray)NSKeyValueCodingAdditions.Utility.valueForKeyPath(parentSelection(), defaultChildKey() + ".@flatten")); appendChildPopupStringWithParent(returnString, parentSelection()); } else { // nothing is selected in the parent, so set the children array to all possible child values // run through all parents, getting each child. However, if we don't have a parentPopUpStringForAll then we only do it for the last parent. if (parentPopUpStringForAll() != null) { appendChildPopupStringWithParent(returnString, parentEntitiesList()); } else { // only do the last parent because we don't have a selected parent AND we don't have the possibility // of setting the parent to 'All' Object aParent = parentEntitiesList().objectAtIndex(0); setChildrenSelection(defaultChildKey()!=null ? (NSArray)NSKeyValueCodingAdditions.Utility.valueForKeyPath(parentSelection(), defaultChildKey() + ".@flatten") : NSArray.EmptyArray); appendChildPopupStringWithParent(returnString, new NSArray(aParent)); } } returnString.append("</select>\n"); jsLog.debug("JSPopUpRelationPicker childPopUpString returnString is {}", returnString); return returnString.toString(); } private void appendChildPopupStringWithParent(StringBuffer returnString, NSArray aParents) { NSArray children = sortedChildren(aParents); // write out each of the values for the options tags. be sure to set the selected tag if necessary int iCount = children.count(); for (int i=0;i<iCount;i++) { Object aChild = children.objectAtIndex(i); returnString.append("\t<option "); if (((i == iCount-1) && (childrenSelection().count() == 0) && (childPopUpStringForAll() == null)) || (isSelectedChild(aChild))) { returnString.append("selected=\"selected\" "); } returnString.append("value=\"" + idForChild(null, aChild) + "\">"); returnString.append(NSKeyValueCodingAdditions.Utility.valueForKeyPath(aChild, childDisplayValueName())); returnString.append("</option>\n"); } } protected StringBuffer selectHeader(String nm, String onChange) { StringBuffer returnString; returnString = new StringBuffer(1000); returnString.append("<select name=\"" + nm + "\"" + " size=\"" + size() + "\""); if (onChange != null) { returnString.append(" onChange=\"" + onChange + "\""); } if (multiple()) { returnString.append(" multiple=\"multiple\""); } returnString.append(">\n"); return returnString; } public String objectArrayCreationString() { // here's an example of the string this method should return: //var parentschildren = new Array(new Entity("dogs","1",new Array(new Entity("poodle","4",null,false),new Entity("puli","5",null,true),new Entity("greyhound","5",null,false)),false), new Entity("fish","2",new Array(new Entity("trout","6",null,true),new Entity("mackerel","7",null,false),new Entity("bass","8",null,false)),true), new Entity("birds","3",new Array(new Entity("robin","9",null,false),new Entity("hummingbird","10",null,false),new Entity("crow","11",null,true)),false)); StringBuilder returnString = new StringBuilder(1000); returnString.append("var "); returnString.append(objectsArrayName); returnString.append(" = ["); int iCount = parentEntitiesList().count(); for (int i=0;i<iCount;i++) { Object aParent = parentEntitiesList().objectAtIndex(i); returnString.append("\n\tnew Entity("); returnString.append(" \"" + NSKeyValueCodingAdditions.Utility.valueForKeyPath(aParent, parentDisplayValueName()) + "\","); returnString.append(" \"" + idForParent(aParent) + "\","); returnString.append(" \"" + System.identityHashCode(aParent) + "\","); // now do all the possible children of the parent. Each child should look like 'new Entity("poodle","4",null,false)' returnString.append(" ["); NSArray childrenOfAParent = sortedChildren(aParent); int jCount = childrenOfAParent.count(); Object defaultChild=defaultChildKey()!=null ? NSKeyValueCodingAdditions.Utility.valueForKeyPath(aParent, defaultChildKey()) : null; int defaultChildIndex=-1; for (int j=0;j<jCount;j++) { Object aChild = childrenOfAParent.objectAtIndex(j); returnString.append("\n\t\t new Entity("); returnString.append(" \"" + NSKeyValueCodingAdditions.Utility.valueForKeyPath(aChild, childDisplayValueName()) + "\","); // visible text of pop-up returnString.append(" \"" + idForChild(aParent, aChild) + "\","); // value text of pop-up returnString.append(" \"" + System.identityHashCode(aChild) + "\","); returnString.append(" null,"); if (isSelectedChild(aChild)) { returnString.append(" true"); } else { returnString.append(" false"); } returnString.append(", null"); returnString.append(')'); if (j != jCount - 1) { // append a comma and a space returnString.append(", "); } if (aChild==defaultChild) defaultChildIndex=j; } returnString.append("],"); if (isSelectedParent(aParent)) { // in the single case, the parent will be updated when we call parent changed returnString.append(" true"); } else { returnString.append(" false"); } returnString.append(", "); returnString.append(defaultChild!=null ? "\""+defaultChildIndex+"\"" : "-1"); returnString.append(')'); if (i != iCount - 1) { // append a comma and a space returnString.append(", "); } } returnString.append("];"); return returnString.toString(); } /** * @param aParent */ private boolean isSelectedParent(Object aParent) { return parentSelection().containsObject(aParent); } /** * @param aChild */ private boolean isSelectedChild(Object aChild) { return childrenSelection().containsObject(aChild); } public NSArray parentEntitiesList() { if(_parentEntitiesList == null) { _parentEntitiesList = (NSArray)valueForBinding("parentEntitiesList"); } return _parentEntitiesList; } public NSArray parentSelection() { if(_parentSelection == null) { if(multiple()) { _parentSelection = (NSArray)valueForBinding("parentSelection"); } else { Object parent = valueForBinding("parentSelection"); if(parent != null) { _parentSelection = new NSArray(parent); } } if(_parentSelection == null) { _parentSelection = NSArray.EmptyArray; } } return _parentSelection; } public void setSelectedParents(NSArray value) { if(!multiple() && canSetValueForBinding("parentSelection")) { setValueForBinding(value == null ? null : value.lastObject(), "parentSelection"); } else if(multiple() && canSetValueForBinding("parentSelection")) { setValueForBinding(value, "parentSelection"); } _parentSelection = value; } public NSArray childrenSelection() { if(_childrenSelection == null) { if(multiple()) { _childrenSelection = (NSArray)valueForBinding("childrenSelection"); } else { Object child = valueForBinding("childrenSelection"); if(child != null) { _childrenSelection = new NSArray(child); } } if(_childrenSelection == null) { _childrenSelection = NSArray.EmptyArray; } } return _childrenSelection; } public void setChildrenSelection(NSArray value) { if(!multiple() && canSetValueForBinding("childrenSelection")) { setValueForBinding(value == null ? null: value.lastObject(), "childrenSelection"); } else if(multiple() && canSetValueForBinding("childrenSelection")) { setValueForBinding(value, "childrenSelection"); } _childrenSelection = value; } public String defaultChildKey() { if(_defaultChildKey == null) { _defaultChildKey = (String)valueForBinding("defaultChildKey"); } return _defaultChildKey; } public String childrenSortKey() { if(_childrenSortKey == null) { _childrenSortKey = (String)valueForBinding("childrenSortKey"); } return _childrenSortKey; } public String childLabel() { if(_childLabel == null) { _childLabel = (String)valueForBinding("childLabel"); if(_childLabel == null) _childLabel = "Types"; } return _childLabel; } public String parentLabel() { if(_parentLabel == null) { _parentLabel = (String)valueForBinding("parentLabel"); if(_parentLabel == null) _parentLabel = "Categories"; } return _parentLabel; } public String childDisplayValueName() { if(_childDisplayValueName == null) { _childDisplayValueName = (String)valueForBinding("childDisplayValueName"); } return _childDisplayValueName; } public String parentDisplayValueName() { if(_parentDisplayValueName == null) { _parentDisplayValueName = (String)valueForBinding("parentDisplayValueName"); } return _parentDisplayValueName; } public String parentToChildrenRelationshipName() { if(_parentToChildrenRelationshipName == null) { _parentToChildrenRelationshipName = (String)valueForBinding("parentToChildrenRelationshipName"); } return _parentToChildrenRelationshipName; } public String parentPopUpStringForAll() { if(_parentPopUpStringForAll == null) { _parentPopUpStringForAll = (String)valueForBinding("parentPopUpStringForAll"); } return _parentPopUpStringForAll; } public String childPopUpStringForAll() { if(_childPopUpStringForAll == null) { _childPopUpStringForAll = (String)valueForBinding("childPopUpStringForAll"); } return _childPopUpStringForAll; } public int size() { if(_size == null) { _size = Integer.valueOf(intValueForBinding("size", multiple() ? 5 : 1)); } return _size.intValue(); } public boolean multiple() { if(_multiple == null) { _multiple = booleanValueForBinding("multiple") ? Boolean.TRUE : Boolean.FALSE; } return _multiple.booleanValue(); } @Override public void reset() { super.reset(); _childrenSelection = null; _parentSelection = null; _parentEntitiesList = null; _childLabel = null; _parentLabel = null; _defaultChildKey = null; _childrenSortKey = null; _multiple = null; _childDisplayValueName = null; _parentDisplayValueName = null; _parentToChildrenRelationshipName = null; _parentPopUpStringForAll = null; _childPopUpStringForAll = null; _possibleChildren = null; _size = null; } }