View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.web.controller.helper;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.krad.uif.UifConstants;
21  import org.kuali.rice.krad.uif.UifParameters;
22  import org.kuali.rice.krad.uif.component.BindingInfo;
23  import org.kuali.rice.krad.uif.container.CollectionGroup;
24  import org.kuali.rice.krad.uif.layout.TableLayoutManager;
25  import org.kuali.rice.krad.uif.util.ColumnSort;
26  import org.kuali.rice.krad.uif.util.ComponentFactory;
27  import org.kuali.rice.krad.uif.util.MultiColumnComparator;
28  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
29  import org.kuali.rice.krad.uif.view.View;
30  import org.kuali.rice.krad.web.form.UifFormBase;
31  
32  import javax.json.Json;
33  import javax.json.JsonArray;
34  import javax.json.JsonObject;
35  import javax.json.JsonReader;
36  import javax.servlet.http.HttpServletRequest;
37  import java.io.StringReader;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.List;
41  
42  /**
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  public class DataTablesPagingHelper {
46  
47      private int totalCollectionSize;
48      private Integer filteredCollectionSize;
49  
50      private TableLayoutManager tableLayoutManager;
51  
52      public void processPagingRequest(View view, String tableId, UifFormBase form, DataTablesInputs dataTablesInputs) {
53          // Set property to trigger special JSON rendering logic in uifRender.ftl
54          form.setRequestJsonTemplate(UifConstants.TableToolsValues.JSON_TEMPLATE);
55  
56          if (view != null) { // avoid blowing the stack if the session expired
57              // don't set the component to update unless we have a postedView, otherwise we'll get an NPE later
58              form.setUpdateComponentId(tableId);
59  
60              @SuppressWarnings("unchecked") List<ColumnSort> oldColumnSorts =
61                      (List<ColumnSort>) form.getExtensionData().get(tableId + UifConstants.IdSuffixes.COLUMN_SORTS);
62  
63              // Create references that we'll need beyond the synchronized block here.
64              CollectionGroup newCollectionGroup = null;
65              List<Object> modelCollection = null;
66              List<ColumnSort> newColumnSorts = null;
67  
68              synchronized (view) { // only one concurrent request per view please
69  
70                  CollectionGroup oldCollectionGroup = (CollectionGroup) view.getViewIndex().getComponentById(tableId);
71                  newColumnSorts = buildColumnSorts(view, dataTablesInputs, oldCollectionGroup);
72  
73                  // get the collection for this group from the model
74                  modelCollection = ObjectPropertyUtils.getPropertyValue(form,
75                          oldCollectionGroup.getBindingInfo().getBindingPath());
76  
77                  applyTableJsonSort(modelCollection, oldColumnSorts, newColumnSorts, oldCollectionGroup, view);
78  
79                  // get a new instance of the collection group component that we'll run the lifecycle on
80                  newCollectionGroup = (CollectionGroup) ComponentFactory.getNewInstanceForRefresh(form.getPostedView(),
81                          tableId);
82  
83                  // set up the collection group properties related to paging in the collection group to set the bounds for
84                  // what needs to be rendered
85                  newCollectionGroup.setUseServerPaging(true);
86                  newCollectionGroup.setDisplayStart(dataTablesInputs.iDisplayStart);
87                  newCollectionGroup.setDisplayLength(dataTablesInputs.iDisplayLength);
88  
89                  // run lifecycle on the table component and update in view
90                  view.getViewHelperService().performComponentLifecycle(view, form, newCollectionGroup,
91                          oldCollectionGroup.getId());
92              }
93  
94              this.tableLayoutManager = (TableLayoutManager) newCollectionGroup.getLayoutManager();
95              this.filteredCollectionSize = newCollectionGroup.getFilteredCollectionSize();
96              this.totalCollectionSize = modelCollection.size();
97  
98              // these other params above don't need to stay in the form after this request, but <tableId>_columnSorts
99              // does so that we avoid re-sorting on each request.
100             form.getExtensionData().put(tableId + "_columnSorts", newColumnSorts);
101         }
102     }
103 
104     /**
105      * Extract the sorting information from the DataTablesInputs into a more generic form.
106      *
107      * @param view posted view containing the collection
108      * @param dataTablesInputs the parsed request data from dataTables
109      * @return the List of ColumnSort elements representing the requested sort columns, types, and directions
110      */
111     private List<ColumnSort> buildColumnSorts(View view, DataTablesInputs dataTablesInputs, CollectionGroup collectionGroup) {
112         int[] sortCols = dataTablesInputs.iSortCol_; // cols being sorted on (for multi-col sort)
113         boolean[] sortable = dataTablesInputs.bSortable_; // which columns are sortable
114         String[] sortDir = dataTablesInputs.sSortDir_; // direction to sort
115 
116         // parse table options to gather the sort types
117         String aoColumnDefsValue = (String) view.getViewIndex().getPostContextEntry(collectionGroup.getId(),
118                 UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
119         JsonArray jsonColumnDefs = null;
120 
121         if (!StringUtils.isEmpty(aoColumnDefsValue)) { // we'll parse this using a JSON library to make things simpler
122             // function definitions are not allowed in JSON
123             aoColumnDefsValue = aoColumnDefsValue.replaceAll("function\\([^)]*\\)\\s*\\{[^}]*\\}", "\"REDACTED\"");
124             JsonReader jsonReader = Json.createReader(new StringReader(aoColumnDefsValue));
125             jsonColumnDefs = jsonReader.readArray();
126         }
127 
128         List<ColumnSort> columnSorts = new ArrayList<ColumnSort>(sortCols.length);
129 
130         for (int sortColsIndex = 0; sortColsIndex < sortCols.length; sortColsIndex++) {
131             int sortCol = sortCols[sortColsIndex]; // get the index of the column being sorted on
132 
133             if (sortable[sortCol]) {
134                 String sortType = getSortType(jsonColumnDefs, sortCol);
135                 ColumnSort.Direction sortDirection = ColumnSort.Direction.valueOf(sortDir[sortColsIndex].toUpperCase());
136                 columnSorts.add(new ColumnSort(sortCol, sortDirection, sortType));
137             }
138         }
139 
140         return columnSorts;
141     }
142 
143     /**
144      * Get the sort type string from the parsed column definitions object.
145      *
146      * @param jsonColumnDefs the JsonArray representation of the aoColumnDefs property from the RichTable template
147      * options
148      * @param sortCol the index of the column to get the sort type for
149      * @return the name of the sort type specified in the template options, or the default of "string" if none is
150      *         found.
151      */
152     private String getSortType(JsonArray jsonColumnDefs, int sortCol) {
153         String sortType = "string"; // default to string if nothing is spec'd
154 
155         if (jsonColumnDefs != null) {
156             JsonObject column = jsonColumnDefs.getJsonObject(sortCol);
157 
158             if (column.containsKey("sType")) {
159                 sortType = column.getString("sType");
160             }
161         }
162         return sortType;
163     }
164 
165     /**
166      * Sort the given modelCollection (in place) according to the specified columnSorts.
167      *
168      * <p>Not all columns will necessarily be directly mapped to the modelCollection, so the collectionGroup and view
169      * are available as well for use in calculating those other column values.  However, if all the columns are in fact
170      * mapped to the elements of the modelCollection, subclasses should be able to easily override this method to
171      * provide custom sorting logic.</p>
172      *
173      * <p>
174      * Create an index array and sort that. The array slots represents the slots in the modelCollection, and
175      * the values are indices to the elements in the modelCollection.  At the end, we'll re-order the
176      * modelCollection so that the elements are in the collection slots that correspond to the array locations.
177      *
178      * A small example may be in order.  Here's the incoming modelCollection:
179      *
180      * modelCollection = { "Washington, George", "Adams, John", "Jefferson, Thomas", "Madison, James" }
181      *
182      * Initialize the array with its element references all matching initial positions in the modelCollection:
183      *
184      * reSortIndices = { 0, 1, 2, 3 }
185      *
186      * After doing our sort in the array (where we sort indices based on the values in the modelCollection):
187      *
188      * reSortIndices = { 1, 2, 3, 0 }
189      *
190      * Then, we go back and apply that ordering to the modelCollection:
191      *
192      * modelCollection = { "Adams, John", "Jefferson, Thomas", "Madison, James", "Washington, George" }
193      *
194      * Why do it this way instead of just sorting the modelCollection directly?  Because we may need to know
195      * the original index of the element e.g. for the auto sequence column.
196      * </p>
197      *
198      * @param modelCollection the collection to sort
199      * @param oldColumnSorts the sorting that reflects the current state of the collection
200      * @param newColumnSorts the sorting to apply to the collection
201      * @param collectionGroup the CollectionGroup that is being rendered
202      * @param view the view
203      */
204     protected void applyTableJsonSort(List<Object> modelCollection, List<ColumnSort> oldColumnSorts,
205             List<ColumnSort> newColumnSorts, CollectionGroup collectionGroup, View view) {
206 
207         boolean isCollectionEmpty = CollectionUtils.isEmpty(modelCollection);
208         boolean isSortingSpecified = !CollectionUtils.isEmpty(newColumnSorts);
209         boolean isSortOrderChanged = newColumnSorts != oldColumnSorts && !newColumnSorts.equals(oldColumnSorts);
210 
211         if (!isCollectionEmpty && isSortingSpecified && isSortOrderChanged) {
212             Integer[] sortIndices = new Integer[modelCollection.size()];
213             for (int i = 0; i < sortIndices.length; i++) {
214                 sortIndices[i] = i;
215             }
216 
217             Arrays.sort(sortIndices, new MultiColumnComparator(modelCollection, collectionGroup, newColumnSorts, view));
218 
219             // apply the sort to the modelCollection
220             Object[] sorted = new Object[sortIndices.length];
221             for (int i = 0; i < sortIndices.length; i++) {
222                 sorted[i] = modelCollection.get(sortIndices[i]);
223             }
224             for (int i = 0; i < sorted.length; i++) {
225                 modelCollection.set(i, sorted[i]);
226             }
227         }
228     }
229 
230     public int getTotalCollectionSize() {
231         return totalCollectionSize;
232     }
233 
234     public Integer getFilteredCollectionSize() {
235         return filteredCollectionSize;
236     }
237 
238     public TableLayoutManager getTableLayoutManager() {
239         return tableLayoutManager;
240     }
241 
242     /**
243      * Input command processor for supporting DataTables server-side processing.
244      *
245      * @see <a href="http://datatables.net/usage/server-side">http://datatables.net/usage/server-side</a>
246      */
247     public static class DataTablesInputs {
248         private static final String DISPLAY_START = "iDisplayStart";
249         private static final String DISPLAY_LENGTH = "iDisplayLength";
250         private static final String COLUMNS = "iColumns";
251         private static final String REGEX = "bRegex";
252         private static final String REGEX_PREFIX = "bRegex_";
253         private static final String SORTABLE_PREFIX = "bSortable_";
254         private static final String SORTING_COLS = "iSortingCols";
255         private static final String SORT_COL_PREFIX = "iSortCol_";
256         private static final String SORT_DIR_PREFIX = "sSortDir_";
257         private static final String DATA_PROP_PREFIX = "mDataProp_";
258         private static final String ECHO = "sEcho";
259 
260         private final int iDisplayStart, iDisplayLength, iColumns, iSortingCols, sEcho;
261 
262         // TODO: All search related options are commented out of this class.
263         // If we implement search for datatables we'll want to re-activate that code to capture the configuration
264         // values from the request
265 
266         //        private final String sSearch;
267         //        private final Pattern patSearch;
268 
269         private final boolean bRegex;
270         private final boolean[] /*bSearchable_,*/ bRegex_, bSortable_;
271         private final String[] /*sSearch_,*/ sSortDir_, mDataProp_;
272 
273         //        private final Pattern[] patSearch_;
274 
275         private final int[] iSortCol_;
276 
277         public DataTablesInputs(HttpServletRequest request) {
278             String s;
279             iDisplayStart = (s = request.getParameter(DISPLAY_START)) == null ? 0 : Integer.parseInt(s);
280             iDisplayLength = (s = request.getParameter(DISPLAY_LENGTH)) == null ? 0 : Integer.parseInt(s);
281             iColumns = (s = request.getParameter(COLUMNS)) == null ? 0 : Integer.parseInt(s);
282             bRegex = (s = request.getParameter(REGEX)) == null ? false : new Boolean(s);
283 
284             //            patSearch = (sSearch = request.getParameter("sSearch")) == null
285             //                    || !bRegex ? null : Pattern.compile(sSearch);
286             //            bSearchable_ = new boolean[iColumns];
287             //            sSearch_ = new String[iColumns];
288             //            patSearch_ = new Pattern[iColumns];
289 
290             bRegex_ = new boolean[iColumns];
291             bSortable_ = new boolean[iColumns];
292 
293             for (int i = 0; i < iColumns; i++) {
294 
295                 //                bSearchable_[i] = (s = request.getParameter("bSearchable_" + i)) == null ? false
296                 //                        : new Boolean(s);
297 
298                 bRegex_[i] = (s = request.getParameter(REGEX_PREFIX + i)) == null ? false : new Boolean(s);
299 
300                 //                patSearch_[i] = (sSearch_[i] = request.getParameter("sSearch_"
301                 //                        + i)) == null
302                 //                        || !bRegex_[i] ? null : Pattern.compile(sSearch_[i]);
303 
304                 bSortable_[i] = (s = request.getParameter(SORTABLE_PREFIX + i)) == null ? false : new Boolean(s);
305             }
306 
307             iSortingCols = (s = request.getParameter(SORTING_COLS)) == null ? 0 : Integer.parseInt(s);
308             iSortCol_ = new int[iSortingCols];
309             sSortDir_ = new String[iSortingCols];
310 
311             for (int i = 0; i < iSortingCols; i++) {
312                 iSortCol_[i] = (s = request.getParameter(SORT_COL_PREFIX + i)) == null ? 0 : Integer.parseInt(s);
313                 sSortDir_[i] = request.getParameter(SORT_DIR_PREFIX + i);
314             }
315 
316             mDataProp_ = new String[iColumns];
317 
318             for (int i = 0; i < iColumns; i++) {
319                 mDataProp_[i] = request.getParameter(DATA_PROP_PREFIX + i);
320             }
321 
322             sEcho = (s = request.getParameter(ECHO)) == null ? 0 : Integer.parseInt(s);
323         }
324 
325         @Override
326         public String toString() {
327             StringBuilder sb = new StringBuilder(super.toString());
328             sb.append("\n\t" + DISPLAY_START + " = ");
329             sb.append(iDisplayStart);
330             sb.append("\n\t" + DISPLAY_LENGTH + " = ");
331             sb.append(iDisplayLength);
332             sb.append("\n\t" + COLUMNS + " = ");
333             sb.append(iColumns);
334 
335             //            sb.append("\n\tsSearch = ");
336             //            sb.append(sSearch);
337 
338             sb.append("\n\t" + REGEX + " = ");
339             sb.append(bRegex);
340 
341             for (int i = 0; i < iColumns; i++) {
342 
343                 //                sb.append("\n\tbSearchable_").append(i).append(" = ");
344                 //                sb.append(bSearchable_[i]);
345 
346                 //                sb.append("\n\tsSearch_").append(i).append(" = ");
347                 //                sb.append(sSearch_[i]);
348 
349                 sb.append("\n\t").append(REGEX_PREFIX).append(i).append(" = ");
350                 sb.append(bRegex_[i]);
351                 sb.append("\n\t").append(SORTABLE_PREFIX).append(i).append(" = ");
352                 sb.append(bSortable_[i]);
353             }
354 
355             sb.append("\n\t").append(SORTING_COLS);
356             sb.append(iSortingCols);
357 
358             for (int i = 0; i < iSortingCols; i++) {
359                 sb.append("\n\t").append(SORT_COL_PREFIX).append(i).append(" = ");
360                 sb.append(iSortCol_[i]);
361                 sb.append("\n\t").append(SORT_DIR_PREFIX).append(i).append(" = ");
362                 sb.append(sSortDir_[i]);
363             }
364 
365             for (int i = 0; i < iColumns; i++) {
366                 sb.append("\n\t").append(DATA_PROP_PREFIX).append(i).append(" = ");
367                 sb.append(mDataProp_[i]);
368             }
369 
370             sb.append("\n\t" + ECHO + " = ");
371             sb.append(sEcho);
372 
373             return sb.toString();
374         }
375     }
376 }