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