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}