001 /** 002 * Copyright 2005-2013 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.rice.krad.web.controller.helper; 017 018 import java.io.StringReader; 019 import java.util.ArrayList; 020 import java.util.Arrays; 021 import java.util.List; 022 023 import javax.json.Json; 024 import javax.json.JsonArray; 025 import javax.json.JsonObject; 026 import javax.json.JsonReader; 027 import javax.servlet.http.HttpServletRequest; 028 import javax.servlet.http.HttpServletResponse; 029 030 import org.apache.commons.collections.CollectionUtils; 031 import org.apache.commons.lang.StringUtils; 032 import org.kuali.rice.krad.uif.UifConstants; 033 import org.kuali.rice.krad.uif.container.CollectionGroup; 034 import org.kuali.rice.krad.uif.layout.TableLayoutManager; 035 import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 036 import org.kuali.rice.krad.uif.util.ColumnSort; 037 import org.kuali.rice.krad.uif.util.ComponentFactory; 038 import org.kuali.rice.krad.uif.util.MultiColumnComparator; 039 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 040 import org.kuali.rice.krad.uif.view.View; 041 import org.kuali.rice.krad.web.form.UifFormBase; 042 043 /** 044 * @author Kuali Rice Team (rice.collab@kuali.org) 045 */ 046 public class DataTablesPagingHelper { 047 048 private int totalCollectionSize; 049 private Integer filteredCollectionSize; 050 051 private TableLayoutManager tableLayoutManager; 052 053 public void processPagingRequest(String tableId, UifFormBase form, 054 HttpServletRequest request, HttpServletResponse response, 055 DataTablesInputs dataTablesInputs) { 056 // Set property to trigger special JSON rendering logic in uifRender.ftl 057 form.setRequestJsonTemplate(UifConstants.TableToolsValues.JSON_TEMPLATE); 058 059 View view = form.getPostedView(); 060 061 if (view != null) { // avoid blowing the stack if the session expired 062 // don't set the component to update unless we have a postedView, otherwise we'll get an NPE later 063 form.setUpdateComponentId(tableId); 064 065 @SuppressWarnings("unchecked") List<ColumnSort> oldColumnSorts = 066 (List<ColumnSort>) form.getExtensionData().get(tableId + UifConstants.IdSuffixes.COLUMN_SORTS); 067 068 // Create references that we'll need beyond the synchronized block here. 069 CollectionGroup newCollectionGroup = null; 070 List<Object> modelCollection = null; 071 List<ColumnSort> newColumnSorts = null; 072 073 synchronized (view) { // only one concurrent request per view please 074 075 CollectionGroup oldCollectionGroup = (CollectionGroup) view.getViewIndex().getComponentById(tableId); 076 newColumnSorts = buildColumnSorts(view, dataTablesInputs, oldCollectionGroup); 077 078 // get the collection for this group from the model 079 modelCollection = ObjectPropertyUtils.getPropertyValue(form, 080 oldCollectionGroup.getBindingInfo().getBindingPath()); 081 082 applyTableJsonSort(modelCollection, oldColumnSorts, newColumnSorts, oldCollectionGroup, view); 083 084 // get a new instance of the collection group component that we'll run the lifecycle on 085 newCollectionGroup = (CollectionGroup) ComponentFactory.getNewInstanceForRefresh(form.getPostedView(), 086 tableId); 087 088 // set up the collection group properties related to paging in the collection group to set the bounds for 089 // what needs to be rendered 090 newCollectionGroup.setUseServerPaging(true); 091 newCollectionGroup.setDisplayStart(dataTablesInputs.iDisplayStart); 092 newCollectionGroup.setDisplayLength(dataTablesInputs.iDisplayLength); 093 094 // run lifecycle on the table component and update in view 095 ViewLifecycle.performComponentLifecycle(view, form, request, response, 096 newCollectionGroup, oldCollectionGroup.getId()); 097 } 098 099 this.tableLayoutManager = (TableLayoutManager) newCollectionGroup.getLayoutManager(); 100 this.filteredCollectionSize = newCollectionGroup.getFilteredCollectionSize(); 101 this.totalCollectionSize = modelCollection.size(); 102 103 // these other params above don't need to stay in the form after this request, but <tableId>_columnSorts 104 // does so that we avoid re-sorting on each request. 105 form.getExtensionData().put(tableId + "_columnSorts", newColumnSorts); 106 } 107 } 108 109 /** 110 * Extract the sorting information from the DataTablesInputs into a more generic form. 111 * 112 * @param view posted view containing the collection 113 * @param dataTablesInputs the parsed request data from dataTables 114 * @return the List of ColumnSort elements representing the requested sort columns, types, and directions 115 */ 116 private List<ColumnSort> buildColumnSorts(View view, DataTablesInputs dataTablesInputs, CollectionGroup collectionGroup) { 117 int[] sortCols = dataTablesInputs.iSortCol_; // cols being sorted on (for multi-col sort) 118 boolean[] sortable = dataTablesInputs.bSortable_; // which columns are sortable 119 String[] sortDir = dataTablesInputs.sSortDir_; // direction to sort 120 121 // parse table options to gather the sort types 122 String aoColumnDefsValue = (String) view.getViewIndex().getPostContextEntry(collectionGroup.getId(), 123 UifConstants.TableToolsKeys.AO_COLUMN_DEFS); 124 JsonArray jsonColumnDefs = null; 125 126 if (!StringUtils.isEmpty(aoColumnDefsValue)) { // we'll parse this using a JSON library to make things simpler 127 // function definitions are not allowed in JSON 128 aoColumnDefsValue = aoColumnDefsValue.replaceAll("function\\([^)]*\\)\\s*\\{[^}]*\\}", "\"REDACTED\""); 129 JsonReader jsonReader = Json.createReader(new StringReader(aoColumnDefsValue)); 130 jsonColumnDefs = jsonReader.readArray(); 131 } 132 133 List<ColumnSort> columnSorts = new ArrayList<ColumnSort>(sortCols.length); 134 135 for (int sortColsIndex = 0; sortColsIndex < sortCols.length; sortColsIndex++) { 136 int sortCol = sortCols[sortColsIndex]; // get the index of the column being sorted on 137 138 if (sortable[sortCol]) { 139 String sortType = getSortType(jsonColumnDefs, sortCol); 140 ColumnSort.Direction sortDirection = ColumnSort.Direction.valueOf(sortDir[sortColsIndex].toUpperCase()); 141 columnSorts.add(new ColumnSort(sortCol, sortDirection, sortType)); 142 } 143 } 144 145 return columnSorts; 146 } 147 148 /** 149 * Get the sort type string from the parsed column definitions object. 150 * 151 * @param jsonColumnDefs the JsonArray representation of the aoColumnDefs property from the RichTable template 152 * options 153 * @param sortCol the index of the column to get the sort type for 154 * @return the name of the sort type specified in the template options, or the default of "string" if none is 155 * found. 156 */ 157 private String getSortType(JsonArray jsonColumnDefs, int sortCol) { 158 String sortType = "string"; // default to string if nothing is spec'd 159 160 if (jsonColumnDefs != null) { 161 JsonObject column = jsonColumnDefs.getJsonObject(sortCol); 162 163 if (column.containsKey("sType")) { 164 sortType = column.getString("sType"); 165 } 166 } 167 return sortType; 168 } 169 170 /** 171 * Sort the given modelCollection (in place) according to the specified columnSorts. 172 * 173 * <p>Not all columns will necessarily be directly mapped to the modelCollection, so the collectionGroup and view 174 * are available as well for use in calculating those other column values. However, if all the columns are in fact 175 * mapped to the elements of the modelCollection, subclasses should be able to easily override this method to 176 * provide custom sorting logic.</p> 177 * 178 * <p> 179 * Create an index array and sort that. The array slots represents the slots in the modelCollection, and 180 * the values are indices to the elements in the modelCollection. At the end, we'll re-order the 181 * modelCollection so that the elements are in the collection slots that correspond to the array locations. 182 * 183 * A small example may be in order. Here's the incoming modelCollection: 184 * 185 * modelCollection = { "Washington, George", "Adams, John", "Jefferson, Thomas", "Madison, James" } 186 * 187 * Initialize the array with its element references all matching initial positions in the modelCollection: 188 * 189 * reSortIndices = { 0, 1, 2, 3 } 190 * 191 * After doing our sort in the array (where we sort indices based on the values in the modelCollection): 192 * 193 * reSortIndices = { 1, 2, 3, 0 } 194 * 195 * Then, we go back and apply that ordering to the modelCollection: 196 * 197 * modelCollection = { "Adams, John", "Jefferson, Thomas", "Madison, James", "Washington, George" } 198 * 199 * Why do it this way instead of just sorting the modelCollection directly? Because we may need to know 200 * the original index of the element e.g. for the auto sequence column. 201 * </p> 202 * 203 * @param modelCollection the collection to sort 204 * @param oldColumnSorts the sorting that reflects the current state of the collection 205 * @param newColumnSorts the sorting to apply to the collection 206 * @param collectionGroup the CollectionGroup that is being rendered 207 * @param view the view 208 */ 209 protected void applyTableJsonSort(final List<Object> modelCollection, List<ColumnSort> oldColumnSorts, 210 final List<ColumnSort> newColumnSorts, final CollectionGroup collectionGroup, final View view) { 211 212 boolean isCollectionEmpty = CollectionUtils.isEmpty(modelCollection); 213 boolean isSortingSpecified = !CollectionUtils.isEmpty(newColumnSorts); 214 boolean isSortOrderChanged = newColumnSorts != oldColumnSorts && !newColumnSorts.equals(oldColumnSorts); 215 216 if (!isCollectionEmpty && isSortingSpecified && isSortOrderChanged) { 217 Integer[] sortIndices = new Integer[modelCollection.size()]; 218 for (int i = 0; i < sortIndices.length; i++) { 219 sortIndices[i] = i; 220 } 221 222 MultiColumnComparator comparator = 223 new MultiColumnComparator(modelCollection, collectionGroup, newColumnSorts, view); 224 Arrays.sort(sortIndices, comparator); 225 226 // apply the sort to the modelCollection 227 Object[] sorted = new Object[sortIndices.length]; 228 for (int i = 0; i < sortIndices.length; i++) { 229 sorted[i] = modelCollection.get(sortIndices[i]); 230 } 231 for (int i = 0; i < sorted.length; i++) { 232 modelCollection.set(i, sorted[i]); 233 } 234 } 235 } 236 237 public int getTotalCollectionSize() { 238 return totalCollectionSize; 239 } 240 241 public Integer getFilteredCollectionSize() { 242 return filteredCollectionSize; 243 } 244 245 public TableLayoutManager getTableLayoutManager() { 246 return tableLayoutManager; 247 } 248 249 /** 250 * Input command processor for supporting DataTables server-side processing. 251 * 252 * @see <a href="http://datatables.net/usage/server-side">http://datatables.net/usage/server-side</a> 253 */ 254 public static class DataTablesInputs { 255 private static final String DISPLAY_START = "iDisplayStart"; 256 private static final String DISPLAY_LENGTH = "iDisplayLength"; 257 private static final String COLUMNS = "iColumns"; 258 private static final String REGEX = "bRegex"; 259 private static final String REGEX_PREFIX = "bRegex_"; 260 private static final String SORTABLE_PREFIX = "bSortable_"; 261 private static final String SORTING_COLS = "iSortingCols"; 262 private static final String SORT_COL_PREFIX = "iSortCol_"; 263 private static final String SORT_DIR_PREFIX = "sSortDir_"; 264 private static final String DATA_PROP_PREFIX = "mDataProp_"; 265 private static final String ECHO = "sEcho"; 266 267 private final int iDisplayStart, iDisplayLength, iColumns, iSortingCols, sEcho; 268 269 // TODO: All search related options are commented out of this class. 270 // If we implement search for datatables we'll want to re-activate that code to capture the configuration 271 // values from the request 272 273 // private final String sSearch; 274 // private final Pattern patSearch; 275 276 private final boolean bRegex; 277 private final boolean[] /*bSearchable_,*/ bRegex_, bSortable_; 278 private final String[] /*sSearch_,*/ sSortDir_, mDataProp_; 279 280 // private final Pattern[] patSearch_; 281 282 private final int[] iSortCol_; 283 284 public DataTablesInputs(HttpServletRequest request) { 285 String s; 286 iDisplayStart = (s = request.getParameter(DISPLAY_START)) == null ? 0 : Integer.parseInt(s); 287 iDisplayLength = (s = request.getParameter(DISPLAY_LENGTH)) == null ? 0 : Integer.parseInt(s); 288 iColumns = (s = request.getParameter(COLUMNS)) == null ? 0 : Integer.parseInt(s); 289 bRegex = (s = request.getParameter(REGEX)) == null ? false : new Boolean(s); 290 291 // patSearch = (sSearch = request.getParameter("sSearch")) == null 292 // || !bRegex ? null : Pattern.compile(sSearch); 293 // bSearchable_ = new boolean[iColumns]; 294 // sSearch_ = new String[iColumns]; 295 // patSearch_ = new Pattern[iColumns]; 296 297 bRegex_ = new boolean[iColumns]; 298 bSortable_ = new boolean[iColumns]; 299 300 for (int i = 0; i < iColumns; i++) { 301 302 // bSearchable_[i] = (s = request.getParameter("bSearchable_" + i)) == null ? false 303 // : new Boolean(s); 304 305 bRegex_[i] = (s = request.getParameter(REGEX_PREFIX + i)) == null ? false : new Boolean(s); 306 307 // patSearch_[i] = (sSearch_[i] = request.getParameter("sSearch_" 308 // + i)) == null 309 // || !bRegex_[i] ? null : Pattern.compile(sSearch_[i]); 310 311 bSortable_[i] = (s = request.getParameter(SORTABLE_PREFIX + i)) == null ? false : new Boolean(s); 312 } 313 314 iSortingCols = (s = request.getParameter(SORTING_COLS)) == null ? 0 : Integer.parseInt(s); 315 iSortCol_ = new int[iSortingCols]; 316 sSortDir_ = new String[iSortingCols]; 317 318 for (int i = 0; i < iSortingCols; i++) { 319 iSortCol_[i] = (s = request.getParameter(SORT_COL_PREFIX + i)) == null ? 0 : Integer.parseInt(s); 320 sSortDir_[i] = request.getParameter(SORT_DIR_PREFIX + i); 321 } 322 323 mDataProp_ = new String[iColumns]; 324 325 for (int i = 0; i < iColumns; i++) { 326 mDataProp_[i] = request.getParameter(DATA_PROP_PREFIX + i); 327 } 328 329 sEcho = (s = request.getParameter(ECHO)) == null ? 0 : Integer.parseInt(s); 330 } 331 332 @Override 333 public String toString() { 334 StringBuilder sb = new StringBuilder(super.toString()); 335 sb.append("\n\t" + DISPLAY_START + " = "); 336 sb.append(iDisplayStart); 337 sb.append("\n\t" + DISPLAY_LENGTH + " = "); 338 sb.append(iDisplayLength); 339 sb.append("\n\t" + COLUMNS + " = "); 340 sb.append(iColumns); 341 342 // sb.append("\n\tsSearch = "); 343 // sb.append(sSearch); 344 345 sb.append("\n\t" + REGEX + " = "); 346 sb.append(bRegex); 347 348 for (int i = 0; i < iColumns; i++) { 349 350 // sb.append("\n\tbSearchable_").append(i).append(" = "); 351 // sb.append(bSearchable_[i]); 352 353 // sb.append("\n\tsSearch_").append(i).append(" = "); 354 // sb.append(sSearch_[i]); 355 356 sb.append("\n\t").append(REGEX_PREFIX).append(i).append(" = "); 357 sb.append(bRegex_[i]); 358 sb.append("\n\t").append(SORTABLE_PREFIX).append(i).append(" = "); 359 sb.append(bSortable_[i]); 360 } 361 362 sb.append("\n\t").append(SORTING_COLS); 363 sb.append(iSortingCols); 364 365 for (int i = 0; i < iSortingCols; i++) { 366 sb.append("\n\t").append(SORT_COL_PREFIX).append(i).append(" = "); 367 sb.append(iSortCol_[i]); 368 sb.append("\n\t").append(SORT_DIR_PREFIX).append(i).append(" = "); 369 sb.append(sSortDir_[i]); 370 } 371 372 for (int i = 0; i < iColumns; i++) { 373 sb.append("\n\t").append(DATA_PROP_PREFIX).append(i).append(" = "); 374 sb.append(mDataProp_[i]); 375 } 376 377 sb.append("\n\t" + ECHO + " = "); 378 sb.append(sEcho); 379 380 return sb.toString(); 381 } 382 } 383 }