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 }