package er.ajax;
import java.text.Format;
import java.text.ParseException;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WODisplayGroup;
import com.webobjects.appserver.WOResponse;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import er.extensions.components._private.ERXWORepetition;
import er.extensions.eof.ERXSortOrdering;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.foundation.ERXValueUtilities;
/**
* Ajax powered grid based on HTML Table that provides drag and drop column
* re-ordering, complex sorting, and the ability to embed components in cells.
* Class names and spans are used extensively to allow the display to be heavily
* customized with CSS.
* <p>
* The data is taken from a WODisplayGroup. Use
* er.extensions.ERXBatchingDisplayGroup to provide high performance access to
* large data sets.
* <p>
* Navigation between batches is not implemented as implementing in it another
* component bound to the display group will allow for a more flexible UI.
* <p>
* <h3>Configuration</h3>
* Configuration is provided by an NSMutableDictionary and NSMutableArray data
* structure. This reduces the number of bindings, eases keeping related lists
* of information in synch, and provides an easy path for serializing a user's
* customizations to configuration for persistent storage. Described as a plist,
* the format of the configuration information is:
*
* <pre>
* {
* tableID = "exampleAjaxGrid"; // Goes on main table tag
* updateContainerID = "ajaxGridContainer"; // Goes on div wrapping table, used with AjaxUpdate* components
* updateFrequency = 60; // Optional frequency of automatic updates of the grid contents
* // This function uses the Ajax.PeriodicalUpdater which does an
* // update when it is first created, rather than waiting for the
* // frequency time before making the first request
* cssClass = "ajaxGrid"; // CSS class attribute on main table tag, optional
* cssStyle = "border: thin solid #000000;"; // CSS style attribute on main table tag, optional
* evenRowCSSClass = "yellowBackground"; // CSS class attribute on even rows, optional
* oddRowCSSClass = "greyBackground"; // CSS class attribute on odd rows, optional
* evenRowCSSStyle = "background:lightyellow;" // CSS style attribute on even rows, optional
* oddRowCSSStyle = "background:lightgrey;"; // CSS style attribute on odd rows, optional
* showRowSelector = true; // Optional, defaults to true, if false no UI is shown to select a row - the cell has only &nbsp;
* selectedRowCSSClass = "yellowBackground"; // Secondary CSS class attribute on selected rows, optional
* unselectedRowCSSClass = "greyBackground"; // Secondary CSS class attribute on unselected rows, optional
* selectedRowCSSStyle = "background:lightyellow;"; // Secondary CSS style attribute on selected rows, optional
* unselectedRowCSSStyle = "background:lightgrey;";// Secondary CSS style attribute on unselected rows, optional
* canReorder = true; // Optional, defaults to true, Enables (or disables) drag and drop reordering of columns
* canResort = true; // Optional, defaults to true, Enables (or disables) sorting by clicking the column titles
* dragHeaderOnly = true; // Optional, defaults to false, true if only the title/header cells can
* // be dragged to re-order columns
* batchSize = 10; // Controls size of batch in display group, use zero for no batching
* rowIdentifier = adKey; // Optional, key path into row returning a unique identifier for the row
* // rowIdentifier is used to build HTML ID attributes, so a String or Number works best
* er.extensions.ERXWORepetition.checkHashCodes = true; // Optional override for global ERXWORepetition setting
*
*
* columns = ( // List of columns to display, controls the initial display order
* {
* title = "Name"; // Title for this column in the table header row
* keyPath = "fullName"; // Key path into row returning the data to display in this colunmn
* }, // keyPath is optional if component is specified
* {
* title = "Department";
* keyPath = "department.name";
* sortPath = "department.code"; // sortPath is an optional path to sort this column on, defaults to keyPath
* // This is useful if keyPath points to something that can't be sorted or a
* // different sort order is need, e.g. here by Department Code rather than name.
* // It can also be used when component is used to provide a sorting for the column
* },
* {
* title = "Hire Date";
* keyPath = "hireDate";
* formatterClass = "com.webobjects.foundation.NSTimestampFormatter"; // Class of formatter to apply to values in this column
* formatPattern = "%m/%d/%y"; // Optional pattern if needed by formatter
* },
* {
* title = "Salary";
* keyPath = "salary";
* formatterClass = "com.webobjects.foundation.NSNumberFormatter";
* formatPattern = "$###,##0.00";
* cssClass = "alignRight"; // CSS class attribute td tag in this column, optional
* },
* {
* title = "Vacation Days";
* keyPath = "daysVacation";
* formatterClass = "com.webobjects.foundation.NSNumberFormatter";
* cssStyle = "text-align: right;"; // CSS style attribute td tag in this column, useful for text-align and width, optional
* },
* {
* title = "Actions";
* keyPath = ""; // Missing or empty keypath results in the current object itself being passed to component
* component = "EmployeeActions"; // Name of WOComponent to be displayed in this column. Gets passed two bindings: value (Object),
* // and grid (AjaxGrid) so that any other needed data can be accessed
* cssClass = "alignCenter";
* }
* );
* sortOrder = (
* {
* keyPath = "department.code"; // If the related column definition uses sortPath, this keyPath should match it
* // This was left as keyPath (rather than sortPath) for backwards compatibility
* direction = "ascending";
* },
* {
* keyPath = "salary";
* direction = "descending";
* },
* {
* keyPath = "daysVacation";
* direction = "ascending";
* }
* ); // This sort is always present so that if the grid is sorted with some ordering where
* mandatorySort = { // identical values span multiple batches, batch membership will not be indeterminate.
* keyPath = "masterAdKey"; // This will only function if this sorting is quite unique (e.g. like a PK).
* direction = "ascending"; // If this sort is also present in sortOrder (above), it will not be duplicated
* }; // but will still always be present. If the user has not manually selected it,
* // it will not be indicated in the UI.
* }
* </pre>
*
* <h4>Initializing Configuration From a File</h4>
* You can get the configuration information from a plist file with code like
* this:
*
* <pre>
* public NSMutableDictionary configData() {
* if (configData == null) {
* // Get data from user preferences here if available, otherwise load the defaults
* configData = mySession().user().preferencesFor("AjaxGridExample");
* if (configData == null) {
* NSData data = new NSData(application().resourceManager().bytesForResourceNamed("AjaxGridExampleConfiguration.plist", null, NSArray.EmptyArray));
* configData = new NSMutableDictionary((NSDictionary) NSPropertyListSerialization.propertyListFromData(data, "UTF-8"));
* }
* }
*
* return configData;
* }
* </pre>
*
* <h3>Updating the Grid From a Different Component</h3>
* When the grid contents are updated the AjaxUpdateContainer needs to be
* updated. The grid configuration <code>updateContainerID</code> gives the ID
* for this. This can be used as the <code>updateContainerID</code> in a
* <code>AjaxUpdateLink</code> or
* <code><updateContainerID>Update()</code> can be called directly. This
* results in a call to <code>ajaxGrid_init('<tableID>');</code> to be
* re-enable drag and drop on the table.
* <p>
* Here is an example. In this example, <code>updateContainerID</code> is used
* to update the grid, and then the <code>NavBar</code> container is updated
* manually from <code>onComplete</code>. The reason for this is that the
* grid needs to update first so that the correct values are available for
* updating the <code>NavBar</code>.
* <p>
* Relevant Configuration:
*
* <pre>
* {
* tableID = "exampleAjaxGrid";
* updateContainerID = "ajaxGridContainer";
* . . .
* </pre>
*
* WOD Bindings:
*
* <pre>
* NavUpdater: AjaxUpdateContainer {
* id = "NavBar";
* }
*
* NextBatch : AjaxUpdateLink {
* action = nextBatch;
* updateContainerID = "ajaxGridContainer";
* onComplete = "function(request) { NavBarUpdate(); }";
* }
* </pre>
*
* <h3>CSS Classes Used by AjaxGrid</h3>
* In addition to the classes defined in the grid configuration, AjaxGrid uses
* some set class names: <table>
* <tr>
* <th>Class Name</th>
* <th>Used For</th>
* </tr>
* <tr>
* <td>ajaxGridRemoveSorting</td>
* <td>The th of cell containing link to remove all sorting</td>
* </tr>
* <tr>
* <td>ajaxGridColumnTitle</td>
* <td>The th of cells containing column titles</td>
* </tr>
* <tr>
* <td>ajaxGridSortAscending</td>
* <td>The span that wraps index and direction indicator of columns sorted in
* ascending order</td>
* </tr>
* <tr>
* <td>ajaxGridSortDescending</td>
* <td>The span that wraps index and direction indicator of columns sorted in
* descending order</td>
* </tr>
* <tr>
* <td>ajaxGridSelectRow</td>
* <td>The td of cells containing the row selection link</td>
* </tr>
* </table
* <h3>Advanced Styling of the Grid</h3>
* The grid contains several places were there are nested anonymous
* <code>span</code> tags wrapping default text content. These are there so
* that the span wrapping the default content can be set to
* <code>display: none</code> and the content of the outer div given in CSS.
* For this HTML:
*
* <pre>
* <th class="ajaxGridRemoveSorting"><span>X</span><em>&nbsp;</em></th>
* </pre>
*
* The default <b>X</b> can replaced with an image with this CSS:
*
* <pre>
* th.ajaxGridRemoveSorting a span {display: none;}
* th.ajaxGridRemoveSorting a em {
* background-image: url(http://vancouver.global-village.net/WebObjects/Frameworks/JavaDirectToWeb.framework/WebServerResources/trashcan-btn.gif);
* background-repeat:no-repeat;
* padding-right: 12px;
* padding-bottom: 5px;
* }
* </pre>
*
* You can also use <code>content</code> to replace the span content with text
* if your browser (not IE) supports it.
*
* <h3>Updating the Configuration</h3>
* If you update the configuration after the grid has been displayed, some items will not update as the information is cached. Add a value
* under the key AjaxGrid.CONFIGURATION_UPDATED to notify the grid to discard the cached information. The grid will remove the value under
* this key after it has cleared the cache.
* <h3>To Do</h3>
* <ul>
* <li>wrap JavaScript in function literal for namespace protection</li>
* <li>drag ghost of column instead of changing column background color when
* dragging. See <a
* href="http://www.webreference.com/programming/javascript/mk/column2/index.html">reference</a>.</li>
* <li>support multiple grids on a single page</li>
* <li>make stateless</li>
* <li>allow auto configuration from .woo file</li>
* <li>allow sorting to be enabled / disabled</li>
* </ul>
*
* @binding displayGroup required, WODisplayGroup acting as source and batching
* engine for the data to be displayed
* @binding configurationData required, NSMutableDictionary used to configure
* grid, see documentation for details
* @binding selectedObjects optional, NSMutableArray list of rows that the user
* has selected from the grid
* @binding willUpdate optional, Ajax action method called when the
* AjaxUpdateContainer is being updated, but before it renders its
* content
* @binding afterUpdate optional, JavaScript to execute client-side after the
* grid has updated
* @binding updateContainerParameters optional, passed as parameters binding to
* the AjaxUpdateContainer wrapping the grid
*
* @property er.extensions.ERXWORepetition.checkHashCodes
*
* @author chill
*/
public class AjaxGrid extends WOComponent {
/**
* 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 WODisplayGroup displayGroup; // binding
private NSMutableArray selectedObjects; // binding
private NSMutableDictionary columnsByKeypath; // optimization
private NSMutableDictionary sortOrdersByKeypath; // optimization
private NSMutableDictionary formattersByKeypath; // optimization
private Boolean showRowSelector; // optimization
private NSKeyValueCodingAdditions row; // local binding
private NSDictionary currentColumn; // local binding
private int rowIndex; // local binding
public static final String TITLE = "title";
public static final String KEY_PATH = "keyPath";
public static final String SORT_PATH = "sortPath";
public static final String SORT_DIRECTION = "direction";
public static final String SORT_ASCENDING = "ascending";
public static final String SORT_DESCENDING = "descending";
public static final String SORT_ORDER = "sortOrder";
public static final String COLUMNS = "columns";
public static final String BATCH_SIZE = "batchSize";
public static final String UPDATE_CONTAINER_ID = "updateContainerID";
public static final String TABLE_ID = "tableID";
public static final String ROW_IDENTIFIER = "rowIdentifier";
public static final String CAN_REORDER = "canReorder";
public static final String CAN_RESORT = "canResort";
public static final String DRAG_HEADER_ONLY = "dragHeaderOnly";
public static final String SOURCE_COLUMN_FORM_VALUE = "sourceColumn";
public static final String DESTINATION_COLUMN_FORM_VALUE = "destinationColumn";
public static final String FORMATTER_CLASS = "formatterClass";
public static final String FORMAT_PATTERN = "formatPattern";
public static final String EVEN_ROW_CSS_CLASS = "evenRowCSSClass";
public static final String ODD_ROW_CSS_CLASS = "oddRowCSSClass";
public static final String EVEN_ROW_CSS_STYLE = "evenRowCSSStyle";
public static final String ODD_ROW_CSS_STYLE = "oddRowCSSStyle";
public static final String SHOW_ROW_SELECTOR = "showRowSelector";
public static final String SELECTED_ROW_CSS_CLASS = "selectedRowCSSClass";
public static final String UNSELECTED_ROW_CSS_CLASS = "unselectedRowCSSClass";
public static final String SELECTED_ROW_CSS_STYLE = "selectedRowCSSStyle";
public static final String UNSELECTED_ROW_CSS_STYLE = "unselectedRowCSSStyle";
public static final String CONFIGURATION_UPDATED = "configurationUpdated";
public static final String COMPONENT_NAME = "component";
public static final String UPDATE_FREQUENCY = "updateFrequency";
public static final String DISPLAY_GROUP_BINDING = "displayGroup";
public static final String CONFIGURATION_DATA_BINDING = "configurationData";
public static final String SELECTED_OBJECTS_BINDING = "selectedObjects";
public static final String WILL_UPDATE_BINDING = "willUpdate";
public static final String AFTER_UPDATE_BINDING = "afterUpdate";
public static final String MANDATORY_SORT_ORDER_FLAG = "isMandatorySortOrder";
public static final String MANDATORY_SORT = "mandatorySort";
public static final String CHECK_HASH_CODES = "er.extensions.ERXWORepetition.checkHashCodes";
public AjaxGrid(WOContext context) {
super(context);
}
/**
* @return false, AjaxGrid is manually synchronized
*/
@Override
public boolean synchronizesVariablesWithBindings() {
return false;
}
/**
* Adds movecolumns.js to the header.
*/
@Override
public void appendToResponse(WOResponse response, WOContext context) {
super.appendToResponse(response, context);
AjaxUtils.addScriptResourceInHead(context, response, "AjaxGrid.js");
}
/**
* Ajax action method for when columns are dragged and dropped. Updates
* configurationData().
*/
public void columnOrderUpdated() {
// The Java script should ensure that these are valid
int sourceIndex = Integer.parseInt((String) context().request().formValueForKey(SOURCE_COLUMN_FORM_VALUE)) - 1;
int destinationIndex = Integer.parseInt((String) context().request().formValueForKey(DESTINATION_COLUMN_FORM_VALUE)) - 1;
Object sourceColumn = columns().objectAtIndex(sourceIndex);
columns().removeObjectAtIndex(sourceIndex);
columns().insertObjectAtIndex(sourceColumn, destinationIndex);
}
/**
* Ajax action method for when column titles are clicked to change sorting.
* Updates configurationData() and displayGroup().
*/
public void sortOrderUpdated() {
// Columns without a key path or sort path can't be sorted
if (currentSortPath() == null || ! canResort()) {
return;
}
NSMutableDictionary sortOrder = currentColumnSortOrder();
// Adding a new sort
if (sortOrder == null) {
NSMutableDictionary newSortOrder = new NSMutableDictionary(2);
newSortOrder.setObjectForKey(currentSortPath(), KEY_PATH);
newSortOrder.setObjectForKey(SORT_ASCENDING, SORT_DIRECTION);
// Keep hidden, mandatory sort as the least important
if (hasMandatorySort() &&
((NSMutableDictionary) sortOrdersByKeypath().objectForKey(manadatorySortKeyPath())).valueForKey(MANDATORY_SORT_ORDER_FLAG) != null) {
sortOrders().insertObjectAtIndex(newSortOrder, sortOrders().count() - 1);
}
else {
sortOrders().addObject(newSortOrder);
}
clearCachedConfiguration();
}
// Making the mandatory sort into an explicit sort
else if (sortOrder.valueForKey(MANDATORY_SORT_ORDER_FLAG) != null) {
sortOrder.removeObjectForKey(MANDATORY_SORT_ORDER_FLAG);
sortOrder.setObjectForKey(SORT_ASCENDING, SORT_DIRECTION);
}
// Changing sort direction
else {
String direction = (String) sortOrder.objectForKey(SORT_DIRECTION);
sortOrder.setObjectForKey(SORT_ASCENDING.equals(direction) ? SORT_DESCENDING : SORT_ASCENDING, SORT_DIRECTION);
}
updateDisplayGroupSort();
}
/**
* Ajax action method for when control clicked to remove all sorting.
* Updates configurationData() and displayGroup().
*/
public void removeSorting() {
if (canResort()) {
configurationData().setObjectForKey(hasMandatorySort() ? new NSMutableArray(manadatorySortDictionary()) : new NSMutableArray(), SORT_ORDER);
clearCachedConfiguration();
updateDisplayGroupSort();
}
}
/**
* @return ID to be used on AjaxUpdateLink bound to removeSorting()
*/
public String removeSortingID() {
return tableID() + "_RemoveSorting";
}
/** @return <code>true</code> if the configuration has anything under the MANDATORY_SORT key */
public boolean hasMandatorySort() {
return configurationData().objectForKey(MANDATORY_SORT) != null;
}
/**
* Returns a copy of the mandatory sort configuration with an
* additional (dummy) value for the MANDATORY_SORT_ORDER_FLAG.
*
* @see #sortOrders()
* @return a dictionary of the mandatory sort
*/
public NSDictionary manadatorySortDictionary() {
NSMutableDictionary manadatorySortDictionary = (NSMutableDictionary) configurationData().objectForKey(MANDATORY_SORT);
manadatorySortDictionary = manadatorySortDictionary.mutableClone();
manadatorySortDictionary.setObjectForKey(Boolean.TRUE, MANDATORY_SORT_ORDER_FLAG);
return manadatorySortDictionary;
}
/** @return value for KEY_PATH under MANDATORY_SORT, the path of the mandatory sort */
public String manadatorySortKeyPath() {
NSMutableDictionary manadatorySortDictionary = (NSMutableDictionary) configurationData().objectForKey(MANDATORY_SORT);
return (String) manadatorySortDictionary.objectForKey(KEY_PATH);
}
/**
* This method is called when the AjaxUpdateContainer is about to update. It
* invokes the willUpdate action binding, if set, and discards the result.
*
*/
public void containerUpdated() {
if (hasBinding(WILL_UPDATE_BINDING)) {
valueForBinding(WILL_UPDATE_BINDING);
}
updateBatchSize();
}
/**
* Script that goes on this page to initialize drag and drop on the grid
* when the page loads / re-loads
*
* @return JavaScript to initialize drag and drop on the grid
*/
public String initScript() {
return canReorder() ? "<script type=\"text/javascript\">AjaxGrid.ajaxGrid_init('" + tableID() + "', " + dragHeaderOnly() + ");</script>" : null;
}
/**
* @return the value for the DRAG_HEADER_ONLY configuration item with a default of false
*/
public String dragHeaderOnly()
{
String dragHeaderOnly = (String) configurationData().objectForKey(DRAG_HEADER_ONLY);
return dragHeaderOnly == null ? "false" : dragHeaderOnly;
}
/**
* Binding value for onRefreshComplete function of AjaxUpdate container.
* Returns the value from the AFTER_UPDATE_BINDING followed by
* enableDragAndDrop().
*
* @return value for AFTER_UPDATE_BINDING concatenated with
* enableDragAndDrop()
*/
public String afterUpdate() {
String afterUpdate = hasBinding(AFTER_UPDATE_BINDING) ? (String) valueForBinding(AFTER_UPDATE_BINDING) : "";
afterUpdate += enableDragAndDrop();
return afterUpdate.length() > 0 ? afterUpdate : null;
}
/**
* Returns Javacript to (re)initialize drag and drop on the grid.
*
* @return ajaxGrid_init(TABLE);
*/
public String enableDragAndDrop() {
return canReorder() ? "AjaxGrid.ajaxGrid_init('" + tableID() + "', " + dragHeaderOnly() + ");" : "";
}
/**
* @return the configurationData
*/
public NSMutableDictionary configurationData() {
NSMutableDictionary configurationData = (NSMutableDictionary) valueForBinding(CONFIGURATION_DATA_BINDING);
if (configurationData.objectForKey(CONFIGURATION_UPDATED) != null)
{
clearCachedConfiguration();
configurationData.removeObjectForKey(CONFIGURATION_UPDATED);
}
return configurationData;
}
/**
* Clears local cache of configuration data so that fresh data will be
* cached.
*
*/
protected void clearCachedConfiguration() {
columnsByKeypath = null;
sortOrdersByKeypath = null;
formattersByKeypath = null;
showRowSelector = null;
}
/**
* Returns CAN_REORDER value from configurationData(), or <code>true</code> if not
* configured.
*
* @return <code>true</code> if column re-ordering is enabled
*/
public boolean canReorder() {
return configurationData().valueForKey(CAN_REORDER) != null ? Boolean.valueOf((String) configurationData().valueForKey(CAN_REORDER)).booleanValue() : true;
}
/**
* Returns CAN_RESORT value from configurationData(), or <code>true</code> if not
* configured.
*
* @return <code>true</code> if data sorting is enabled
*/
public boolean canResort() {
return configurationData().valueForKey(CAN_RESORT) != null ? Boolean.valueOf((String) configurationData().valueForKey(CAN_RESORT)).booleanValue() : true;
}
/**
* Returns TABLE_ID value from configurationData()
*
* @return HTML ID for <table> implementing the grid
*/
public String tableID() {
return (String) configurationData().valueForKey(TABLE_ID);
}
/**
* Returns an optional key path into row that will return a value that uniquely identifies this row.
* This should be suitable for use as part of an HTML ID attribute.
*
* @return an optional key path into row that will return a value that uniquely identifies this row
*/
public String rowIdentifier() {
return (String) configurationData().valueForKey(ROW_IDENTIFIER);
}
/**
* Returns COLUMNS value from configurationData()
*
* @return list of configuration for the columns to display in the grid
*/
protected NSMutableArray columns() {
return (NSMutableArray) configurationData().valueForKey(COLUMNS);
}
/**
* Returns SORT_ORDER value from configurationData()
*
* @return list of sort orders controlling display of data in the grid
*/
protected NSMutableArray sortOrders() {
NSMutableArray sortOrders = (NSMutableArray) configurationData().valueForKey(SORT_ORDER);
// Add the mandatory sort if it is not present
if (hasMandatorySort()) {
boolean includesMandatorySort = false;
for (int i = 0; i < sortOrders.count() && ! includesMandatorySort; i++) {
if (((NSKeyValueCoding) sortOrders.objectAtIndex(i)).valueForKey(KEY_PATH).equals(manadatorySortKeyPath())) {
includesMandatorySort = true;
}
}
if ( ! includesMandatorySort) {
sortOrders.addObject(manadatorySortDictionary());
}
}
return sortOrders;
}
/**
* Returns BATCH_SIZE value from configurationData()
*
* @return batch size for the display grid
*/
protected int batchSize() {
Object batchSizeObj = configurationData().objectForKey(AjaxGrid.BATCH_SIZE);
return ERXValueUtilities.intValue(batchSizeObj);
}
/**
* Returns EVEN_ROW_CSS_CLASS or ODD_ROW_CSS_CLASS, depending on rowIndex(),
* value from configurationData() followed by SELECTED_ROW_CSS_CLASS or
* UNSELECTED_ROW_CSS_CLASS, depending on isRowSelected(), value from
* configurationData()
*
* @return CSS class for this row
*/
public String rowClass() {
boolean isEven = rowIndex() % 2 == 0;
String userClass = (String) configurationData().valueForKey(isEven ? EVEN_ROW_CSS_CLASS : ODD_ROW_CSS_CLASS);
String selectionClass = (String) configurationData().valueForKey(isRowSelected() ? SELECTED_ROW_CSS_CLASS : UNSELECTED_ROW_CSS_CLASS);
if (userClass == null) {
return selectionClass;
}
if (selectionClass == null) {
return userClass;
}
return userClass + " " + selectionClass;
}
/**
* Returns EVEN_ROW_CSS_STYLE or ODD_ROW_CSS_STYLE, depending on rowIndex(),
* value from configurationData() folowed by SELECTED_ROW_CSS_STYLE or
* UNSELECTED_ROW_CSS_STYLE, depending on isRowSelected(), value from
* configurationData()
*
* @return CSS class for this row
*/
public String rowStyle() {
boolean isEven = rowIndex() % 2 == 0;
String userStyle = (String) configurationData().valueForKey(isEven ? EVEN_ROW_CSS_STYLE : ODD_ROW_CSS_STYLE);
String selectionStyle = (String) configurationData().valueForKey(isRowSelected() ? SELECTED_ROW_CSS_STYLE : UNSELECTED_ROW_CSS_STYLE);
if (userStyle == null) {
return selectionStyle;
}
if (selectionStyle == null) {
return userStyle;
}
return userStyle + " " + selectionStyle;
}
/**
* Returns a value suitable for use as part of an HTML ID attribute that uniquely identifies this row. If rowIdentifier()
* is set, used row().valueForKeyPath(rowIdentifier()), otherwise uses "row_" and the index of the row in the list of objects.
*
* @return a value suitable for use as part of an HTML ID attribute that uniquely identifies this row
*/
public String rowID() {
if (rowIdentifier() != null) {
return ERXStringUtilities.safeIdentifierName(row().valueForKeyPath(rowIdentifier()).toString());
}
return "row_" + String.valueOf(rowIndex());
}
/**
* Returns a key into this row that produces a unique value for this row.
*
* @return a key into this row that produces a unique value for this row
*/
public String rowIdentifierKey() {
return rowIdentifier() != null ? rowIdentifier() : "hashCode";
}
/**
* @return dictionary of columns() keyed on KEY_PATH of column
*/
public NSMutableDictionary columnsByKeypath() {
if (columnsByKeypath == null) {
NSArray columns = columns();
columnsByKeypath = new NSMutableDictionary(columns.count());
for (int i = 0; i < columns.count(); i++) {
columnsByKeypath.setObjectForKey(columns.objectAtIndex(i), ((NSKeyValueCoding) columns.objectAtIndex(i)).valueForKey(KEY_PATH));
}
}
return columnsByKeypath;
}
/**
* @return dictionary of sortOrders() keyed on KEY_PATH of column
*/
public NSMutableDictionary sortOrdersByKeypath() {
if (sortOrdersByKeypath == null) {
NSArray sortOrders = sortOrders();
sortOrdersByKeypath = new NSMutableDictionary(sortOrders.count());
for (int i = 0; i < sortOrders.count(); i++) {
sortOrdersByKeypath.setObjectForKey(sortOrders.objectAtIndex(i), ((NSKeyValueCoding) sortOrders.objectAtIndex(i)).valueForKey(KEY_PATH));
}
}
return sortOrdersByKeypath;
}
/**
* @return dictionary of formatters for columns() keyed on KEY_PATH of
* column
*/
public NSMutableDictionary formattersByKeypath() {
if (formattersByKeypath == null) {
NSArray columns = columns();
formattersByKeypath = new NSMutableDictionary(columns.count());
for (int i = 0; i < columns.count(); i++) {
NSDictionary column = (NSDictionary) columns.objectAtIndex(i);
String className = (String) column.valueForKey(FORMATTER_CLASS);
if (className != null) {
try {
Format formatter = (Format) Class.forName(className).newInstance();
String pattern = (String) column.valueForKey(FORMAT_PATTERN);
if (pattern != null) {
NSKeyValueCoding.DefaultImplementation.takeValueForKey(formatter, pattern, "pattern");
}
formattersByKeypath.setObjectForKey(formatter, column.valueForKey(KEY_PATH));
}
catch (Exception e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
}
}
}
}
return formattersByKeypath;
}
/**
* Updates sort orders on the display group. This is public and static so that a display group can be pre-configured
* for the AjaxGrid to avoid re-fetching (happens if there is a nav bar that gets rendered before the grid).
*
* @param dg the WODisplayGroup to set the sortOrderings of
* @param sortConfig NSArray of sorts from grid configuration (not EOSortOrderings)
*/
public static void updateDisplayGroupSort(WODisplayGroup dg, NSArray sortConfig) {
NSMutableArray sortOrders = new NSMutableArray(sortConfig.count());
for (int i = 0; i < sortConfig.count(); i++) {
NSDictionary column = (NSDictionary) sortConfig.objectAtIndex(i);
sortOrders.addObject(new ERXSortOrdering((String) column.objectForKey(KEY_PATH), (SORT_ASCENDING.equals(column.objectForKey(SORT_DIRECTION)) ? EOSortOrdering.CompareCaseInsensitiveAscending : EOSortOrdering.CompareCaseInsensitiveDescending)));
}
// Only set this if there has been an actual change to avoid discarding fetched objects
if (! sortOrders.equals(dg.sortOrderings()) ) {
dg.setSortOrderings(sortOrders);
dg.updateDisplayedObjects();
}
}
/**
* Updates sort orders on the display group.
*/
protected void updateDisplayGroupSort() {
updateDisplayGroupSort(displayGroup(), sortOrders());
}
/**
* Updates numberOfObjectsPerBatch on the display group. This is public and static so that a display group can be pre-configured
* for the AjaxGrid to avoid re-fetching (happens if there is a nav bar that gets rendered before the grid).
*
* @param dg the WODisplayGroup to set the sortOrderings of
* @param batchSize batch size from grid configuration (not EOSortOrderings)
*/
public static void updateBatchSize(WODisplayGroup dg, int batchSize) {
if (dg.numberOfObjectsPerBatch() != batchSize) {
dg.setNumberOfObjectsPerBatch(batchSize);
}
}
/**
* Updates numberOfObjectsPerBatch on the display group
*/
protected void updateBatchSize() {
updateBatchSize(displayGroup(), batchSize());
}
/**
* @return the displayGroup
*/
public WODisplayGroup displayGroup() {
if (displayGroup == null) {
displayGroup = (WODisplayGroup) valueForBinding(DISPLAY_GROUP_BINDING);
updateDisplayGroupSort();
updateBatchSize();
}
return displayGroup;
}
/**
* @return ID to be used on AjaxUpdateLink bound to sortOrderUpdated() for currentColumn()
*/
public String currentColumnID() {
StringBuilder b = new StringBuilder(tableID());
b.append("_SortBy_");
b.append(ERXStringUtilities.safeIdentifierName((String)currentColumn().objectForKey(TITLE)));
b.append(isCurrentColumnSortedAscending() ? "_Descending" : "_Ascending");
return b.toString();
}
/**
* @return <code>true</code> if currentColumn() is part of the sort
* ordering but is not the mandatory sort
*/
public boolean isCurrentColumnSorted() {
return currentColumnSortOrder() != null && currentColumnSortOrder().objectForKey(MANDATORY_SORT_ORDER_FLAG) == null;
}
/**
* @return the sort order dictionary for currentColumn() or null if !
* isCurrentColumnSorted()
*/
public NSMutableDictionary currentColumnSortOrder() {
return (NSMutableDictionary) sortOrdersByKeypath().objectForKey(currentSortPath());
}
/**
* @return <code>true</code> if currentColumn() is part of the sort
* ordering and is being sorted in ascending order
*/
public boolean isCurrentColumnSortedAscending() {
return isCurrentColumnSorted() ? SORT_ASCENDING.equals(currentColumnSortOrder().valueForKey(SORT_DIRECTION)) : false;
}
/**
* @return the index (1 based) of this columns precedence in sorting or -1
* if it is not part of the sort order
*/
public int currentColumnSortIndex() {
return isCurrentColumnSorted() ? sortOrders().indexOf(currentColumnSortOrder()) + 1 : -1;
}
/**
* @return the value from row() corresponding to currentColumn(), formatted
* as configured
*/
public Object columnValue() {
// Special case when there is no keyPath: return the row object. This is
// intended for custom components
// where there is no specific value
if (currentKeyPath() == null || currentKeyPath().length() == 0) {
return row();
}
Object rawValue = row().valueForKeyPath(currentKeyPath());
Format formatter = columnFormatter();
return (formatter != null && rawValue != null) ? formatter.format(rawValue) : rawValue;
}
/**
* Method used with automatic synchronizing custom components.
*
* @param value
* new value for row() in this column
*/
public void setColumnValue(Object value) {
// Special case when there is no keyPath: there is nothing to set
if (currentKeyPath() == null || currentKeyPath().length() == 0) {
return;
}
Format formatter = columnFormatter();
if (formatter != null && value instanceof String) {
try {
value = formatter.parseObject((String) value);
}
catch (ParseException e) {
throw new NSForwardException(e);
}
}
row().takeValueForKey(value, currentKeyPath());
}
/**
* @return the value from row() corresponding to currentColumn()
*/
public Format columnFormatter() {
return (Format) formattersByKeypath().valueForKey(currentKeyPath());
}
/**
* @return the keyPath value from currentColumn()
*/
public String currentKeyPath() {
return (String) currentColumn().valueForKey(KEY_PATH);
}
/**
* @return the sortPath value from currentColumn() or currentKeyPath() if not found
*/
public String currentSortPath() {
return currentColumn().valueForKey(SORT_PATH) == null ? currentKeyPath() : (String) currentColumn().valueForKey(SORT_PATH);
}
/**
* @return the name of the WOComponent to use to display the value from
* currentColumn()
*/
public String columnComponentName() {
String componentName = (String) currentColumn().valueForKey(COMPONENT_NAME);
return componentName != null ? componentName : "WOString";
}
/**
* @return value of SHOW_ROW_SELECTOR from the configuration data, or <code>true</code> if unset
*/
public boolean showRowSelector() {
if (showRowSelector == null) {
showRowSelector = Boolean.TRUE;
if (configurationData().valueForKey(SHOW_ROW_SELECTOR) != null) {
showRowSelector = Boolean.valueOf((String) configurationData().valueForKey(SHOW_ROW_SELECTOR));
}
}
return showRowSelector.booleanValue();
}
/**
* This list is implemented by AjaxGrid and is not based on the display
* group's selected objects. The list of selected objects is maintained
* across all batches.
*
* @return list of user selected objects
*/
public NSMutableArray selectedObjects() {
if (selectedObjects == null) {
selectedObjects = hasBinding(SELECTED_OBJECTS_BINDING) ? (NSMutableArray) valueForBinding(SELECTED_OBJECTS_BINDING) : new NSMutableArray();
}
return selectedObjects;
}
/**
* Toggles inclusion of row into selectedObjects() (i.e. selects and
* de-selects it).
*/
public void toggleRowSelection() {
if (isRowSelected()) {
selectedObjects().removeObject(row);
}
else {
selectedObjects().addObject(row);
}
}
/**
* @return ID to be used on AjaxUpdateLink bound to toggleRowSelection()
*/
public String toggleRowSelectionID() {
return tableID() + "_ToggleRowSelection_" + rowID();
}
/**
* @return <code>true</code> if row() is in selectedObjects()
*/
public boolean isRowSelected() {
return selectedObjects().containsObject(row);
}
/**
* Cover method for item binding of a WORepetition.
*
* @return the current column being rendered
*/
public NSDictionary currentColumn() {
return currentColumn;
}
/**
* Cover method for item binding of a WORepetition.
*
* @param newColumn
* current column being rendered
*/
public void setCurrentColumn(NSDictionary newColumn) {
currentColumn = newColumn;
}
/**
* Cover method for item binding of a WORepetition.
*
* @return the current row being rendered
*/
public NSKeyValueCodingAdditions row() {
return row;
}
/**
* Cover method for item binding of a WORepetition.
*
* @param newRow
* current row being rendered
*/
public void setRow(NSKeyValueCodingAdditions newRow) {
row = newRow;
}
/**
* Cover method for index binding of a WORepetition.
*
* @return index of the current row being rendered
*/
public int rowIndex() {
return rowIndex;
}
/**
* Cover method for index binding of a WORepetition.
*
* @param index
* inded of current row being rendered
*/
public void setRowIndex(int index) {
rowIndex = index;
}
/**
* @return this component so that it can be used in bindings
*/
public AjaxGrid thisComponent() {
return this;
}
/**
* Allows configuration data to override ERXWORepetition default for checkHashCodes.
*
* @return boolean value for CHECK_HASH_CODES if set in config data, otherwise the default from ERXWORepetition
*/
public boolean checkHashCodes() {
if (configurationData().objectForKey(CHECK_HASH_CODES) == null) {
configurationData().setObjectForKey(Boolean.toString(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXWORepetition.checkHashCodes",
ERXProperties.booleanForKey(ERXWORepetition.class.getName() + ".checkHashCodes"))),
CHECK_HASH_CODES);
}
return Boolean.parseBoolean((String)configurationData().objectForKey(CHECK_HASH_CODES));
}
}