View Javadoc
1   /**
2    * Copyright 2005-2014 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.uif.layout.collections;
17  
18  import java.io.StringReader;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.List;
22  
23  import javax.json.Json;
24  import javax.json.JsonArray;
25  import javax.json.JsonObject;
26  import javax.json.JsonReader;
27  import javax.servlet.http.HttpServletRequest;
28  
29  import org.apache.commons.collections.CollectionUtils;
30  import org.apache.commons.lang.StringUtils;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.container.CollectionGroup;
33  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
34  import org.kuali.rice.krad.uif.util.ColumnSort;
35  import org.kuali.rice.krad.uif.util.MultiColumnComparator;
36  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
37  import org.kuali.rice.krad.uif.view.View;
38  import org.kuali.rice.krad.uif.view.ViewModel;
39  
40  /**
41   * @author Kuali Rice Team (rice.collab@kuali.org)
42   */
43  public class DataTablesPagingHelper {
44  
45      public static void processPagingRequest(View view, ViewModel form, CollectionGroup collectionGroup,
46              DataTablesInputs dataTablesInputs) {
47          if (view == null) {
48              return;
49          }
50  
51          String collectionGroupId = collectionGroup.getId();
52  
53          List<ColumnSort> newColumnSorts;
54          synchronized (view) {
55              // get the collection for this group from the model
56              List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(form,
57                      collectionGroup.getBindingInfo().getBindingPath());
58  
59              List<ColumnSort> oldColumnSorts =
60                      (List<ColumnSort>) ViewLifecycle.getViewPostMetadata().getComponentPostData(collectionGroupId,
61                              UifConstants.IdSuffixes.COLUMN_SORTS);
62  
63              newColumnSorts = buildColumnSorts(view, form, dataTablesInputs, collectionGroup);
64  
65              applyTableJsonSort(modelCollection, oldColumnSorts, newColumnSorts, collectionGroup, form, view);
66  
67              // set up the collection group properties related to paging in the collection group to set the bounds for
68              // what needs to be rendered
69              collectionGroup.setUseServerPaging(true);
70              collectionGroup.setDisplayStart(dataTablesInputs.iDisplayStart);
71              collectionGroup.setDisplayLength(dataTablesInputs.iDisplayLength);
72          }
73  
74          // these other params above don't need to stay in the form after this request, but <collectionGroupId>_columnSorts
75          // does so that we avoid re-sorting on each request.
76          ViewLifecycle.getViewPostMetadata().addComponentPostData(collectionGroupId,
77                  UifConstants.IdSuffixes.COLUMN_SORTS, newColumnSorts);
78      }
79  
80      /**
81       * Extract the sorting information from the DataTablesInputs into a more generic form.
82       *
83       * @param form object containing the view's data
84       * @param view posted view containing the collection
85       * @param dataTablesInputs the parsed request data from dataTables
86       * @return the List of ColumnSort elements representing the requested sort columns, types, and directions
87       */
88      private static List<ColumnSort> buildColumnSorts(View view, ViewModel form, DataTablesInputs dataTablesInputs,
89              CollectionGroup collectionGroup) {
90          int[] sortCols = dataTablesInputs.iSortCol_; // cols being sorted on (for multi-col sort)
91          boolean[] sortable = dataTablesInputs.bSortable_; // which columns are sortable
92          String[] sortDir = dataTablesInputs.sSortDir_; // direction to sort
93  
94          // parse table options to gather the sort types
95          String aoColumnDefsValue = (String) form.getViewPostMetadata().getComponentPostData(collectionGroup.getId(),
96                  UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
97  
98          JsonArray jsonColumnDefs = null;
99          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 }